Understanding Exception Handling in JavaScript

malcolmkee

Malcolm Kee

Posted on January 18, 2022

Understanding Exception Handling in JavaScript

When writing code, we’re make assumptions, implicitly or explicitly.

As an example, let say you wrote a simple multiply function like below:

function multiply(x, y) {
  return x * y;
}
Enter fullscreen mode Exit fullscreen mode

multiply function has an implicit assumption that both parameters (x and y) are both numbers.

// When the assumption is correct, all is fine.
multiply(2, 5); // -> 10

// When the assumption is incorrect
multiply('abcd', '234'); // -> NaN
multiply(new Date(), 2); // -> 32849703863543284970386354
Enter fullscreen mode Exit fullscreen mode

How to Handle Incorrect Assumption

Although the example above seems trivial, its impact may not so trivial once you realize that a simple function could be used for important operation, such as calculating how much your customer pay you:

function getCartData() {
  // here we always return the same result, but just imagine that
    // in real life, it call some API to get the data
  return { 
        items: [{ quantity: 2, unitPrice: 50 }, { quantity: 1, unitPrice: 'USD 5' }]
    };
}

function issueInvoice(total) {
  // some code that will generate invoice to customer
}

function getCartTotal(items) {
  let total = 0;

  for (const item of items) {
    /* one of the item.unitPrice is 'USD 23.00' 
    (instead of number),
    which will cause the total become NaN */
    total += multiply(item.unitPrice, item.quantity);
  }

  return total;
}

function chargeCustomer(cart) {
  const total = getCartTotal(cart.items);
  // when total is NaN, issueInvoice 
  // will generate invoice with 0 amount!
  issueInvoice(total);
}

function checkout() {
  const cartData = getCartData();
  chargeCustomer(cartData);
}
Enter fullscreen mode Exit fullscreen mode

To properly fix the issue, we need to fix the code that incorrectly set the unitPrice as 'USD 23.00' instead of 23. However, sometimes the code that generates the data is out of our control, e.g. it could be maintained by other team, or it could be code from other company.

So how do we deal with incorrect assumption in code?

1. Assume less

The first approach of dealing with assumptions is to eliminate them.

We can change our multiply function to below:

// multiply will returns undefined if either parameter is not number
function multiply(x, y) {
  if (typeof x !== 'number' || typeof y !== 'number') {
    return undefined;
  }
  return x * y;
}
Enter fullscreen mode Exit fullscreen mode

And then the code that calls multiply should handle both number and undefined as returned result of the call.

// getCartTotal will returns undefined if the computation could not complete
function getCartTotal(items) {
  let total = 0;

  for (const item of items) {
    const subtotal = multiply(item.unitPrice, item.quantity);
    if (typeof subtotal === 'undefined') {
        alert(`Subtotal is not number, something is wrong!`);
      return undefined;
    } else {
        total += subtotal;
    }
  }

  return total;
}

function chargeCustomer(cart) {
  const total = getCartTotal(cart.items);
  // when total is undefined, issueInvoice will not be run
  if (typeof total === 'undefined') {
        issueInvoice(total);
  }
}
Enter fullscreen mode Exit fullscreen mode

As you may already observed, although assuming less works, but it makes the code more complicated as there are more conditional logics now.

2. throw Error

Fortunately, JavaScript (and most modern programming languages) allows us to handle exception case like above using throw, for example

function multiply(x, y) {
  if (typeof x !== 'number' || typeof y !== 'number') {
    throw 'parameters passed to multiply function is not number.';
  }
  return x * y;
}
Enter fullscreen mode Exit fullscreen mode

Now that when multiply is called with either of the parameters is not number, you will see the following in the console, which is great.

Browser console showing error Uncaught parameters passed to multiply function is not number.

More importantly, throw will stop the code execution, so the remaining code will not run.

function getCartTotal(items) {
  let total = 0;

  for (const item of items) {
    /* one of the item.unitPrice is 'USD 23.00' (instead of number),
    which will cause multiply to throw */
    total += multiply(item.unitPrice, item.quantity);
  }

  return total;
}

function chargeCustomer(cart) {
  const total = getCartTotal(cart.items);
  // the following code will not be reached, 
  // as throw will stop the remaining code execution
  issueInvoice(total);
}
Enter fullscreen mode Exit fullscreen mode

Now customer will no longer be getting free stuffs anymore!

Handle error gracefully

Although now we stop the code from giving away free stuffs to customer by using throw, but it would be even better if we can provide a more graceful behavior when that happens, like showing some error message to customer.

We can do that using try ... catch.

function getCartTotal(items) {
  let total = 0;

  for (const item of items) {
    total += multiply(item.unitPrice, item.quantity);
  }

  return total;
}

function chargeCustomer(cart) {
  const total = getCartTotal(cart.items);
  issueInvoice(total);
}

function checkout() {
  try {
    const cartData = getCartData();
    chargeCustomer(cartData);
  } catch (err) {
    // log to console. But can send to error tracking service 
    // if your company use one.
    console.error(err); 
    alert('Checkout fails due to technical error. Try again later.');
  }
}
Enter fullscreen mode Exit fullscreen mode

Now customer will see an error message, instead of just page not responding.

To visualize the code flow, you can refer to the following drawing.

Error handling flow

Best Practices on Using throw with try ... catch

1. Only use it for exceptional case.

Compared to other conditional syntaxes like (if and switch), throw and try ... catch are harder to read because the throw statement and try ... catch may be in totally different part of the codebase.

However, what is considered to be “exceptional” case depends on the context of the code.

  1. For example, if you are writing user-facing code that read the user input, don’t use throw to control the logic to validate user input to show error message. Instead, you should use normal control flow like if .. else.
  2. On the other hand, if you are writing computation code like calculation, you can use throw when the data passed to you is invalid, as we usually assume input is validated on the more user-facing code, so when an invalid data reach you, it probably is some programmatic mistake that rarely happens.

2. throw Error only

Although technically throw any value like string or object, it is common practices to only throw Error.

throw new Error('Something goes wrong that I not sure how to handle');
Enter fullscreen mode Exit fullscreen mode

3. Always console.error in catch.

It is possible that the try ... catch phrase will catch error that is thrown by other code. For example:

try {
  let data = undefined;

    if (data.hasError) {
        throw new Error('Data has error');
  }
} catch (err) {
  console.error(err);
}
Enter fullscreen mode Exit fullscreen mode

At first glance, you may thought that the err in catch phrase is the error thrown with throw new Error('Data has error'). But if you run the code above, the actual error being thrown is TypeError: Cannot read properties of undefined (reading 'hasError'). This is because the line if (data.hasError) { tries to read property from undefined.

Those runtime JavaScript error will be caught by try ... catch as well, so it’s best practices that you always console.error the error to ensure that you are aware of those unexpected programming error.

💖 💪 🙅 🚩
malcolmkee
Malcolm Kee

Posted on January 18, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related