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;
}
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
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);
}
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;
}
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);
}
}
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;
}
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.
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);
}
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.');
}
}
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.
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.
- 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 likeif .. else
. - 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');
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);
}
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.
Top comments (0)