JavaScript Concurrency: Error Handling in Async Functions
Welcome to the Error Handling in Async Functions lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
We've seen that
throwing an exception inside of athencallback causes the promise to reject. The thrown exception is transformed into a rejected promise.>
Promise.resolve().then(() => {throw new Error('it failed');});Async Result:
{rejected: 'Error: it failed'}What about the opposite: transforming a rejected promise into a thrown exception? That's exactly what happens when we
awaita rejected promise. The rejection reason is thrown as a normal JavaScript exception, allowing us tocatchit.>
async function willFail() {try {await Promise.reject(new Error('it failed'));} catch {return 'we caught an exception';}return 'we got to the end';}willFail();Async Result:
{fulfilled: 'we caught an exception'}It's important to note that there's nothing special about this try/catch. It doesn't know anything about promises. Instead, the
awaitexpression throws a regular JavaScript exception, which we then catch in the regular way.What if there's an exception inside of an
asyncfunction, but we don't catch it? When an exception in anasyncfunction is uncaught, it's automatically translated into a rejected promise. This is part of a general rule:asyncfunctions always return promises, no matter what.>
async function willThrow() {throw new Error('it failed');}willThrow();Async Result:
{rejected: 'Error: it failed'}To summarize:
awaitandasyncboth transform between exceptions and rejected promises. Anasyncfunction turns any exception into a rejection. Andawaiting a rejected promise throws an exception.Here's an example that shows both of those facts together. The
asyncfunction hereawaits a rejected promise. It returns a rejected promise where the reason is'Error: it failed'.>
async function willFail() {await Promise.reject(new Error('it failed'));}willFail();Async Result:
{rejected: 'Error: it failed'}It's tempting to look at that example and think, "We awaited a rejected promise, so the async function returned that rejected promise." But here's what actually happened:
- We
awaited a rejected promise. - Awaiting a rejected promise always throws an exception.
- That exception made the function exit early, as it does in any function, whether it's async or not.
- Because it was an async function, the exception was translated into a rejection.
- We
Here's a code problem:
Modify this function to catch the exception caused by
awaiting a rejected promise. Returnnullwhen theawaitthrows an exception.async function getOwnerForCat(cat) {if (cat === undefined) {throw new Error('cat does not exist');} else {/* Find the cat's owner; code omitted. */}}async function run() {try {return await getOwnerForCat(undefined);} catch {return null;}}run();Async Result:
- Goal:
{fulfilled: null}- Yours:
{fulfilled: null}
The interaction between async/await and exceptions may seem complex, and it is complex! However, that complexity results from a deep tension between promises and async/await.
Promises represent asynchronous operations as values. This is nice because we can pass them to functions, return them from functions, store them in variables, etc. But async/await treats asynchronous operations like regular control flow within a JavaScript function. This is nice because we can smoothly mix async/await code with
if,for,while,try, andcatch.Both ways have their own strengths, so we want to be able to mix them as needed. But if we allow both ways to be used, then we need rules for translating between the two, as we've seen in this lesson. The good news is that, as with everything in programming, switching between promises and async/await becomes natural with some practice.
We can now summarize the rules of exceptions in async/await functions:
asyncfunctions always return promises.awaitturns rejected promises into exceptions.- Exceptions inside
asyncfunctions turn into rejected promises.
One final point about exceptions in async functions: there's an important difference between
return somePromiseandreturn await somePromise. Theawaittranslates rejected promises into exceptions. If we directly return the promise, rejections aren't translated into exceptions at all.Here's a pair of examples showing the difference. The first example does
return await aRejectedPromise. It throws, so ourcatchworks.>
async function fail() {try {return await Promise.reject(new Error('oh no'));} catch (e) {return 'caught the error';}}fail();Async Result:
{fulfilled: 'caught the error'}The second example returns the promise directly:
return aRejectedPromise. There's noawait, so the rejection doesn't turn into an exception, so there's nothing for ourcatchto catch. Instead, this example returns the original rejected promise.>
async function fail() {try {return Promise.reject(new Error('oh no'));} catch (e) {return 'caught the error';}}fail();Async Result:
{rejected: 'Error: oh no'}Fortunately, linters can help with this. For example, TypeScript users can use typescript-eslint's return-await rule to make sure that they always
return await. Plain old discipline can also help: if you alwaysawaitpromises inside ofasyncfunctions, this problem won't occur.