Synchronous vs Asynchronous Errors
The asynchronous coding styles used in Javascript can be non-intuitive for developers more familiar with imperative languages. That can be dangerous because things look just familiar enough that people can make dangerous assumptions. That’s compounded by the fact that Javascript has evolved from callbacks, to promises, to async/await very rapidly. It can get complicated if you have to work with code written in a mix of the three different styles.
I’ve seen several instances where developers wrote code using Promises and assumed that their catch
function call would catch any possible errors.
Here’s a simplified example:
function doSomethingAsync() {
throw new Error('synchronous');
// we'll never get here
return new Promise(resolve => setImmediate(() => resolve()));
}
doSomethingAsync()
.then(() => console.log('No error!'))
.catch((err) => {
// we expect to get any errors here
console.error(`Do Something very important! ${err}`);
});
Clearly the person who wrote this code assumes that since doSomething
is expected to return a promise then any errors that occur will be caught in the catch
function call. In fact there might be lots of ways for doSomething
to fail that have nothing to do with it’s asynchronous work. Since Javascript is interpreted we might even get an error for a simple typo or other syntax error.
If we run the code shown above we get:
Error: synchronous
Not what we might have expected. Since the error in our function is not actually using promises the catch
block will never see this error. That can be a big problem if we expect that something important will happen in case of any errors occurring.
Even worse, this can be a hard bug to catch if you’re not looking for it.
In the past I would fix code like this by wrapping the entirety of doSomething
in a try/catch block like:
function doSomethingAsync() {
try {
throw new Error("synchronous");
// we'll never get here
return new Promise(resolve => setImmediate(() => resolve()));
} catch (err) {
// now our function will always return a promise
return Promise.reject(err);
}
}
doSomethingAsync()
.then(() => console.log("No error!"))
.catch((err) => {
console.error(`Do Something very important! ${err}`);
});
That gives us the expected behavior:
Do Something very important! synchronous
Now that async/await is available in Javascript we can handle this without using that try/catch:
async function doSomethingAsync() {
throw new Error("synchronous");
// we'll never get here
return new Promise(resolve => setImmediate(() => resolve()));
}
doSomethingAsync()
.then(() => console.log("No error!"))
.catch((err) => {
console.error(`Do Something very important! ${err}`);
});
Much nicer!