If you’ve kept your JavaScript skills up to date, you’ve probably come across ES6 Promises before. Promises provide a clean, flexible way to chain multiple async operations together without having to nest function calls inside callbacks (which might lead to callback hell). Promises can be powerful but if you don’t follow proper error-handling practices, they can make life difficult.

If you’ve worked on a large project that made extensive use of promises, you may have encountered some difficulties with debugging — This is because, without careful error-handling, Error objects that are thrown inside promises won’t tell you anything about the sequence of actions which led to the error; all they’ll tell you is the file and line number where the error was created. To illustrate the problem; imagine that you have many front-end components which share the same service to load data from a server; if a HTTP request fails, you might know what the error was and where it was created/thrown (the HTTP service) but you won’t know which higher-level component actually caused the issue—A common case might be that one of your components passed invalid arguments to the service. This can be a major pain if you’re dealing with long promise chains that stretch across multiple files.

I did a fair bit of searching online and I couldn’t find any specific advice on how to deal with this problem so I decided to investigate a bit more. The standard way to handle errors from promises is to add a catch handler at the end of your promise chain like this:

// doAsyncOperation1() returns a promise.

doAsyncOperation1()

.then(() => {

// ...

// doAnotherAsyncOperation() returns a promise

// which will be inserted into the chain.

return doAsyncOperation2();

})

.then((output) => {

// output will be the value resolved by the

// promise which was returned from doAsyncOperation2()

// ...

return doAsyncOperation3();

})

.catch((err) => {

// Handle any error that occurred in any of the previous

// promises in the chain.

});

A useful thing about catch handlers is that you can chain them:

// ...

.then((output) => {

return doAsyncOperation3();

})

.catch((err) => {

// Re-throw the error as a higher-level error.

// We will include the message from the lower-level

// error as part of the error message for this error.

throw new Error('Higher-level error. ' + err.message);

})

.catch((err) => {

// In here we will get the higher-level error.

})

Being able to chain multiple catch handlers like this is particularly useful if you have a sub-module which returns a long promise chain which spills out into one or more other higher-level modules; this means that you can add a single catch at the end of each sub-chain (in each file) which simply re-throws errors into the current file’s scope. You don’t always need to refer to the previous error when re-throwing an error but it can sometimes help.

Catching and re-throwing errors in this way will tell you which higher-level module lead to the error; that means that you can later dig in and debug the issue on a layer-by-layer basis (top-down approach).

Note that the technique discussed in this article is mostly useful for handling exceptions, not unexpected errors caused by bugs in the code. To find errors caused by unexpected bugs, you need to test each code unit/module individually.