Error handling is an important part of programs. There are many situations where our programs encounter values that are unexpected and we have to properly handle them.
In this article, we’ll look at how to handle them so that errors are easily found easily and gracefully handled.
Exceptions are Better than Return Codes
Throwing exceptions is better because they let us know that an error exists and that we have to handle it.
Most modern programming languages have exceptions built-in, so we should throw them instead of returning an error code.
Error codes aren’t as explicit and may be missed. Exceptions are also much cleaner since we don’t have to check all the codes that may be returned.
For example, if we return error codes in our functions then we may have code that looks something like this:
We have to return all the error codes in our setNumFruit
method. Also, before we do something after the class definition, we have to check all the error codes.
We can throw exceptions instead:
We’ve eliminated the need to check all the error codes by wrapping the code we want to run in a try
block. Now we can just catch the error instead of checking all the error codes that may be returned.
This is much better than checking all error codes before doing something — it’s especially important as code becomes more complex.
Write Try-Catch-Finally
We should wrap our try
in the code that throws exceptions that we want to catch. It creates its own scope for block-scoped variables so anything declared with let
or const
can only be referenced in the try
block.
Variables declared with var
are hoisted so that they can be referenced outside the block. We won’t get an error even if they’re referenced outside the block. This will get us 1
:
try {
var x = 1;
} catch (ex) {
console.error(ex);
}
console.log(x);
But this will get us Uncaught ReferenceError: x is not defined
:
try {
let x = 1;
} catch (ex) {
console.error(ex);
}
console.log(x);
Don’t Ignore Caught Errors
We should report our errors when they’re caught. They shouldn’t be caught and then ignored. This is because we don’t want to sweep underlying issues under the rug.
Reporting exceptions let us know about the error and then handle it accordingly.
The examples above, like the console.error
, call in the following:
try {
const error = fruitStand.setNumFruit(1);
console.log(fruitStand.numFruits);
} catch (ex) {
console.error(ex);
}
Thi s one of the ways to report the error. We can also use other libraries to report the error in a central place.
Don’t Ignore Rejected Promises
Like any other exception, rejected promises also need to be handled. They can be handled with the callback that we pass into the catch
method or use the try...catch
block for async
functions — they’re the same.
For example, we can report the error by writing the following:
Promise.reject('fail')
.catch(err => {
console.error(err);
})
Or for async
functions we can write this:
(async () => {
try {
await Promise.reject('fail')
} catch (err) {
console.error(err);
}
})()
Providing Context with Exceptions
We should provide enough context in exceptions that other developers can pinpoint the error when an exception is thrown.
We can get a stack trace with exceptions in JavaScript, but it may not provide all the information we need.
Conclusion
When we handle errors, throwing exceptions is better than returning error codes since they let us use the try...catch
block to handle errors. This is much cleaner than checking multiple error codes.
When we throw exceptions, we have to provide enough details to pinpoint the problem.
We shouldn’t just ignore errors after catching them. We should at least report them so that they can be looked at.
Finally, rejected promise errors should be handled the same way as other exceptions.