Skip to main content

Errors in async functions

Errors inside async functions are automatically turned into a rejected promise (Promise.reject). That is why you handle them either with a try...catch block inside the function or with a .catch() method on the promise the function returns.

Theory

TL;DR

  • An async function always returns a promise, and a throw inside it becomes a rejection.
  • await on a rejected promise throws an exception, so an ordinary try...catch catches it.
  • .catch() on the function call is the second, equally valid option, convenient at the edge of the application.
  • One try can cover several await calls, or each await can get its own try for a targeted reaction.
  • A return from the catch block heals the situation: the function's promise fulfils with a default value.
  • In Promise.all one failing promise brings the whole call down, so for partial results you use Promise.allSettled.

Quick example

javascript
async function getData() { try { const res = await fetch("https://no-such-domain.example/data"); const data = await res.json(); console.log(data); } catch (error) { console.error("An error occurred:", error.message); } } getData(); // Output: An error occurred: fetch failed

Here try...catch works exactly as in synchronous code, but it catches errors from every await inside the block.

Why this works

await pauses the function until the promise settles. If the promise is rejected, await raises an exception and control moves to catch. The code after a failed await does not run at all.

javascript
async function example() { const result = await Promise.reject("Failure!"); console.log(result); // will not run } example().catch(console.error); // Output: Failure!

That is the whole trick of async/await: a rejected promise becomes a regular exception, and a regular exception becomes a rejected promise again at the function boundary.

A shared try block and local control

Several await calls can be wrapped in a single try if all the errors are handled the same way:

javascript
async function loadUserData() { try { const user = await fetch("/api/user"); const posts = await fetch("/api/posts"); const comments = await fetch("/api/comments"); } catch (e) { console.error("Failed to load the data:", e); } }

If any of the operations fails, control moves to catch immediately, and the remaining requests never even start.

Sometimes it is better to wrap individual await calls, when the reactions must differ:

javascript
async function example() { try { const user = await fetchUser(); } catch (e) { console.error("Failed to load the user:", e); } try { const posts = await fetchPosts(); } catch (e) { console.error("Failed to load the posts:", e); } }

Without try...catch the promise simply rejects

If you do not wrap await in try...catch, the function just returns a rejected promise, which you handle with .catch() at the call site:

javascript
async function fail() { throw new Error("Something went wrong"); } fail().catch(err => console.error("Caught:", err.message)); // Output: Caught: Something went wrong

This is fully equivalent to .catch() handling on ordinary promises.

The same .catch() can be attached to a single await when you only need to suppress the error and continue with a safe value:

javascript
async function getUser() { const res = await fetch("/api/user").catch(err => { console.error("Request failed:", err); return null; // return a safe value }); console.log("Result:", res); } getUser();

And if you return a value from the catch block, the function fulfils successfully and that value reaches .then():

javascript
async function example() { try { await Promise.reject("Failure!"); } catch (e) { console.log("Caught:", e); return "Default value"; } } example().then(result => console.log("Result:", result)); // Output: // Caught: Failure! // Result: Default value

Errors in parallel await calls

If you run several operations in parallel through Promise.all and at least one promise rejects, the whole Promise.all fails with the first error:

javascript
async function parallel() { try { const [a, b] = await Promise.all([ Promise.resolve("ok"), Promise.reject("failure"), ]); } catch (e) { console.log("Caught:", e); } } parallel(); // Output: Caught: failure

If you need the results of every operation regardless of which ones failed, use Promise.allSettled: it always fulfils and returns an array of objects whose status is either "fulfilled" or "rejected".

Best practices

SituationWhat to use
Handling errors inside one functiontry...catch
Handling errors when calling an async function.catch()
Different error types with different reactionsSeveral separate try...catch blocks
Several parallel await callsPromise.allSettled() instead of Promise.all
Logging and rethrowingcatch (e) { log(e); throw e; }

Common mistakes

  • Forgetting await before the call. The function returns a promise instead of a value, and the error never reaches the try...catch, which has already finished.
  • return promise instead of return await promise inside try. Without await the rejection happens after control has left try, so the local catch never sees it.
  • Calling an async function with no handling at all. That gives an unhandledrejection in the browser and a terminated process in Node.js 15 and newer.
  • Assuming fetch throws on 404 or 500. fetch rejects only on a network failure, so the status has to be checked manually through res.ok.
  • Catching an error and swallowing it silently. An empty catch turns a failure into a success with undefined; either log it and throw e, or return an explicit default value.
  • Sequential await calls where parallel ones are needed. Three independent requests in three await statements run one after another; start them through Promise.all or Promise.allSettled.
  • A try...catch around a setTimeout callback. An exception from a timer callback goes to the global handler, not to your catch; the asynchronous operation has to be wrapped in a promise.

Short Answer

Interview ready
Premium

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