Promise-based functions should not throw exceptions


This blog post gives tips for error handling in asynchronous, Promise-based functions.



Operational errors vs. programmer errors

In programs, there are two kinds of errors:



  • Operational errors happen when a correct program encounters an exceptional situation that requires deviating from the “normal” algorithm. For example, a storage device may run out of memory while the program is writing data to it. This kind of error is expected.



  • Programmer errors happen when code does something wrong. For example, a function may require a parameter to be a string, but receives a number. This kind of error is unexpected.




Operational errors: don’t mix rejections and exceptions

For operational errors, each function should support exactly one way of signaling errors. For Promise-based functions that means not mixing rejections and exceptions, which is the same as saying that they shouldn’t throw exceptions.


Programmer errors: fail quickly

For programmer errors, it usually makes sense to fail as quickly as possible:



function downloadFile(url) {
if (typeof url !== 'string') {
throw new Error('Illegal argument: ' + url);
}
return new Promise(···).
}

Note that this is not a hard and fast rule. You have to decide whether or not you can handle exceptions in a meaningful way in your asynchronous code.


Handling exceptions in Promise-based functions

If exceptions are thrown inside the callbacks of then() and catch() then that’s not a problem, because these two methods convert them to rejections.


However, things are different if you start your async function by doing something synchronous:



function asyncFunc() {
doSomethingSync(); // (A)
return doSomethingAsync()
.then(result => {
···
});
}

If an exception is thrown in line A then the whole function throws an exception. There are two solutions to this problem.


Solution 1: returning a rejected Promise

You can catch exceptions and return them as rejected Promises:



function asyncFunc() {
try {
doSomethingSync();
return doSomethingAsync()
.then(result => {
···
});
} catch (err) {
return Promise.reject(err);
}
}

Solution 2: executing the sync code inside a callback

You can also start a chain of then() method calls via Promise.resolve() and execute the synchronous code inside a callback:



function asyncFunc() {
return Promise.resolve()
.then(() => {
doSomethingSync();
return doSomethingAsync();
})
.then(result => {
···
});
}

An alternative is to start the Promise chain via the Promise constructor:



function asyncFunc() {
return new Promise((resolve, reject) => {
doSomethingSync();
resolve(doSomethingAsync());
})
.then(result => {
···
});
}

This approach saves you a tick (the synchronous code is executed right away), but it makes your code less regular.


Async functions and exceptions

Brian Terlson points out that async functions reflect a preference for not mixing exceptions and rejections: Originally, if an async function had a default value that threw an exception then the function would throw an exception. Now, the function rejects the Promise it returns.


Further reading


Acknowledgements: this post was inspired by a post by user Mörre Noseshine in the “Exploring ES6” Google Group. Im also thankful for the feedback to a tweet asking whether it is OK to throw exceptions from Promise-based functions.




Comments

Popular posts from this blog

Steve Lopez and the Importance of Newspapers

A Treasure Hunt Without The Treasure

Drop a ping-pong ball in the clown’s mouth