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
- “Error Handling in Node.js” by Joyent
- ES proposal: async functions
- Promises for asynchronous programming [chapter in “Exploring ES6”]
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
Post a Comment