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.