JavaScript Promises and Error Handling

Errors in asynchronous code typically require a messy number of if else checks and a careful inspection of parameter values. Promises allow asynchronous code to apply structured error handling. When using promises, you can pass an error handler to the then method or use a catch method to process errors. Just like exceptions in regular code, an exception or rejection in asynchronous code will jump to the nearest error handler.

As an example, let’s use the following functions which log the execution path into a string variable.

var log = "";
 
function doWork() {
    log += "W";
    return Promise.resolve();
}
 
function doError() {
    log += "E";
    throw new Error("oops!");
}
 
function errorHandler(error) {
    log += "H";
}

We’ll use these functions with the following code.

doWork()
    .then(doWork)
    .then(doError)
    .then(doWork) // this will be skipped
    .then(doWork, errorHandler)
    .then(verify);
     
  function verify() {
    expect(log).toBe("WWEH");
    done();
}

The expectation is that the log variable will contain “WWEH” when the code finishes executing, meaning the flow of calls with reach doWork, then doWork, then doError, then errorHandler. There are two observations to make about this result, one obvious, one subtle.

The first observation is that when the call to doError throws an exception, execution jumps to the next rejection handler (errorHandler) and skips over any potential success handlers. This behavior is obvious once you think of promises as a tool to transform asynchronous code into a procedural flow of method calls. In synchronous code, an exception will jump over statements and up the stack to find a catch handler, and the asynchronous code in this example is no different.

What might not be immediately obvious is that the verify function will execute as a success handler after the error. Just like normal execution can resume in procedural code after a catch statement, normal execution can resume with promises after a handled error. Technically, the verify function executes because the error handler returns a successfully resolved promise. Remember the then method always returns a new promise, and unless the error handler explicitly rejects a new promise, the new promise resolves successfully.

A promise object also provides a catch method to handle errors. The last code sample could be written with a catch statement as follows. ]

doWork()
    .then(doWork)
    .then(doError)
    .then(doWork) 
    .then(doWork)
    .catch(errorHandler)
    .then(verify);

The catch method takes only a rejection handler method. There can be a difference in behavior between the following two code snippets:

.then(doWork, errorHandler)

… and …

.then(doWork)
.catch(errorHandler)

In the first code snippet, if the success handler throws an exception or rejects a promise, execution will not go into the error handler since the promise was already resolved at this level. With catch, you can always see an unhandled error from the previous success handler.

Finally, imagine you have a rejected promise in your code, but there is no error handler attached. You can simulate this scenario with the following line of code.

Promise.reject("error!");

Some native environments and promise polyfills will warn you about unhandled promise rejections by displaying a message in the console of the developer tools. An unhandled promise rejection means your application could be missing out on a critical error!