Skip to main content

Error handling in async functions

Every async function always returns a promise, so an error inside it moves that promise into the rejected state. From that follow the two main ways to handle it: try...catch inside the function itself, and .catch() outside, at the call site.

Theory

TL;DR

  • An async function always returns a promise; a throw inside it is a Promise.reject().
  • try...catch inside catches both an explicit throw and rejected promises you await.
  • .catch() outside catches the same error, but at the place where the function is called.
  • The two are combined: inside you log and rethrow a new error, outside you show it to the user.
  • The [err, data] pattern lets you avoid a try...catch at every step.
  • Anything caught nowhere is picked up by the global unhandledrejection handler.

Quick example

javascript
async function getData() { try { const res = await fetch('https://api.github.com/404'); // network error const data = await res.json(); console.log('Received:', data); } catch (error) { console.error('Error inside async:', error.message); } } getData();

Output:

text
Error inside async: Failed to fetch

try...catch inside the function

This is the most familiar and most readable option: the code looks exactly like synchronous code.

try...catch catches:

  • errors thrown with throw;
  • rejected promises that you await.

It works because under the hood await unwraps a rejection into an ordinary exception:

javascript
const result = await Promise.reject('failure'); // throws an exception, control moves into the catch block

In other words, a rejected promise inside an async function behaves exactly like a plain throw, and the same try...catch catches it.

.catch() outside and a manual throw

Since an async function returns a Promise, the error can be handled with the usual .catch():

javascript
async function getUser() { const res = await fetch('https://api.example.com/missing'); return res.json(); } getUser() .then(console.log) .catch(err => console.error('Error outside:', err.message));

Output:

text
Error outside: Failed to fetch

The difference from try...catch is that here the error is handled at the call, not inside the function. A manual throw behaves the same way:

javascript
async function example() { const data = await Promise.resolve('OK'); if (!data) throw new Error('No data!'); return data; } example() .then(console.log) .catch(err => console.error(err.message));

Output:

text
OK

If the condition fires, throw turns into Promise.reject() and the external .catch() receives it.

Catching with a rethrow, and several levels of try...catch

Sometimes an error has to be handled only partly, for example logged, and then passed on:

javascript
async function processData() { try { const res = await fetch('https://api.example.com/missing'); return await res.json(); } catch (err) { console.error('Request error:', err.message); throw new Error('Error while processing data'); } } processData().catch(err => console.error('Final error:', err.message));

Output:

text
Request error: Failed to fetch Final error: Error while processing data

When different stages need different recovery, use several try...catch blocks:

javascript
async function pipeline() { try { const res = await fetch('/api/data'); try { const data = await res.json(); console.log('Data:', data); } catch (err) { console.error('JSON parsing error:', err.message); } } catch (err) { console.error('Request error:', err.message); } }

This approach is useful when a network failure and invalid JSON have to be treated separately.

The [err, data] pattern without try...catch

Sometimes it is more convenient to use a helper that never rejects and returns a pair instead:

javascript
const to = (promise) => promise.then(data => [null, data]).catch(err => [err]); async function run() { const [err, data] = await to(fetch('/api/user').then(r => r.json())); if (err) return console.error('Error:', err.message); console.log(data); }

This pattern is common in Node.js, as a way to avoid writing try...catch at every step and to keep error handling flat.

The global unhandledrejection handler

If .catch() or try...catch was forgotten, the error can still be intercepted globally:

javascript
window.addEventListener('unhandledrejection', (event) => { console.error('Unhandled promise:', event.reason); });

This is the equivalent of try...catch for every promise that has no .catch() of its own. In Node.js, process.on('unhandledRejection', handler) plays the same role. Such a handler is a last line of defence and a place to log, not a replacement for proper handling.

WayWhere it catches the errorExample
try...catchinside the async functiontry { await ... } catch (e) { ... }
.catch()at the function callmyAsync().catch(e => ...)
Global handlerfor everything unhandledwindow.addEventListener('unhandledrejection', ...)
The [err, data] patternthrough a wrapperconst [err, res] = await to(promise)

A simple analogy: an async function is a quest. await is a task that may fail; try...catch is the safety net that lets you decide what to do next; .catch() on the outside is the game master who picks up the fatal failures the player could not handle.

Common mistakes

  • Forgetting await before res.json() in a return: with try { return res.json(); } the parsing error escapes the try block, so the local catch never sees it.
  • Catching an error, doing nothing and not rethrowing it: the caller receives undefined and behaves as if everything succeeded.
  • Assuming fetch throws on HTTP 404 or 500. It rejects only on a network failure, so the status must be checked manually through res.ok.
  • Wrapping a call to an async function in try...catch without await: without await the exception never reaches that block and becomes an unhandled rejection instead.
  • Relying on the global unhandledrejection alone: it fires too late and gives the user no meaningful message.
  • Rethrowing a string instead of an Error: the call stack is lost and err.message is undefined.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.