Error handling in JavaScript can be easy while being tricky at certain places, especially Promises. JS allows error handling with the help of try, catch, and throw.
const main = () => {
try {
// Do something super crazy
if (!bakePizza()) {
throw new Error('Oh no!');
}
} catch (error) {
// That escalated quickly
handleOvenExplosion();
}
}
This seems simple enough but gets a little tricky when Promises are involved.
Let’s look at a simple example of a Promise. The following Promise function fetches a list of user profiles from the database, where the result set is resolved by the promise function and the error is rejected.
const userProfileQuery = new Promise((resolve, reject) => {
connection.query('SELECT * FROM Users', [], (err, result) => {
if (err) reject({ type: 'SQL', err});
connection.release();
resolve(result);
});
userProfileQuery
.then((data) => {
const userList = data;
// Do something crazy with the list
})
.catch((err) => {
// Oh, snap!
// Handle error
});
In an ideal world, we’d want to have a single try-catch block to handle all errors that occur in that single file.
const { getUserProfiles } = require('./helpers');
module.exports = () => {
try {
let userProfileList;
getUserProfiles
.then((data) => {
userProfileList = data;
})
.catch((error) => {
// Handle Promise Error
// All errors thrown in this promise land here
});
} catch (error) {
// Handle errors in this module
}
}
The above module is simple — It fetches a list of user profiles with the help of a Promise function.
But the problem with the above module is that when we throw
a new Error
inside the then
block of the promise, it will always pass to the catch
block of the promise. That is because throwing a new error inside a then
block of a promise will always be passed to the catch
block of the invoking promise function. This does not allow us to handle all errors in a module with a singular try-catch block.
But, alas! There is a way to handle this with the help of Async/Await. Let me explain this better with an example —
const { getUserProfiles } = require('./helpers');
module.exports = async () => {
try {
const userProfileList = await getUserProfiles;
} catch (error) {
// Handle errors in this module
switch (type) {
case ERROR_SQL:
// Handle SQL errors
default:
// Handle common errors
}
}
}
This little addition of async/await in your code does two things —
Assign the value to the variable which was resolved by the promise function.
Throw error if the promise function rejects anything.
Note that the value assignment works only when a promise functions resolves some value and errors get thrown only when the promise function rejects something.
This way, async/await lets us keep our code clean, maintainable, and easy to read.
Thanks for reading. If you have thoughts on this, be sure to leave a comment.
Top comments (5)
Very useful, thank you :) I was just a bit confused by this statement, it seems to contradict itself?:
"But the problem with the above module is that when we throw a new Error inside the then block of the promise, it will always pass to the catch block of the promise. That is because throwing a new error inside a then block of a promise will always be passed to the catch block of the invoking promise function."
I think the catch block it was referring to is the catch block right after the then block. But the goal was to have a single catch block do all the error handling but from that code, it had two catch blocks ( 1. for module errors, 2. for promise/then errors )
@fluffystark pretty much answers your question. However, I'll explain it further just in case -
Say you have a promise function called
somePromise
. When you do athrow new Error()
inside your.then()
block, like belowthe error will get passed to the
.catch()
block ofsomePromise()
and not thecatch
block of thetry-catch
.When having multiple promise statements in your code, it can become cumbersome to handle each promise's errors in its own
.catch()
block. Usingasync/await
allows you to handle all errors in one place. I hope this cleared your doubt. :)Thanks for writing this! Using promises as a wrapper around logic that could fail is a useful pattern. You can also listen for promise errors globally (rather than attaching a catch to each one) using the unhandledrejection global event.
It’s also important to log these sort of failures back to a service like TrackJS to help you understand when things fail in production.
Somewhat off-topic, async/await were introduced in ES8 not ES6.