Suggest an editImprove this articleRefine the answer for “Error handling in async functions”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Every `async` function always returns a promise, so an error inside it simply moves that promise into the `rejected` state, and you can catch it either with `try...catch` inside the function or with `.catch()` outside at the call site.** `try...catch` intercepts both an explicit `throw` and rejected promises that you `await`, because `await` unwraps a rejection into an ordinary exception. An external `.catch()` fires on the result of the call, which suits cases where the caller decides what to do about the failure. The two approaches are often combined: inside you log and rethrow a new `Error`, outside you show it to the user. To avoid many nested `try` blocks, people use a wrapper that returns `[err, data]`, and whatever is caught nowhere is picked up by the global `unhandledrejection` handler. ```javascript async function getData() { try { const res = await fetch('https://api.example.com/missing'); return await res.json(); } catch (error) { console.error('Request failed:', error.message); throw new Error('Data processing failed'); } } ``` **Key point:** an error in `async` is a rejected promise; catch it with `try...catch` inside or `.catch()` outside.Shown above the full answer for quick recall.Answer (EN)Image**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. | Way | Where it catches the error | Example | | --- | --- | --- | | `try...catch` | inside the `async` function | `try { await ... } catch (e) { ... }` | | `.catch()` | at the function call | `myAsync().catch(e => ...)` | | Global handler | for everything unhandled | `window.addEventListener('unhandledrejection', ...)` | | The `[err, data]` pattern | through a wrapper | `const [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`.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.