In a JavaScript async function, how does wrapping an `await` in try/catch let you handle a rejected promise, and what exactly does `await` do when the promise it is waiting on rejects?
answer
- rejection becomes a normal exception
- only at the await point
- catch binding is the raw reason
- async functions never throw synchronously
basics
~20 sawait rethrows a rejected promise's reason as an ordinary exception at the await point, so a normal try/catch around that await catches it. The catch parameter is bound to whatever value the promise rejected with, which need not be an Error.
solid answer
~50 s`await` has two outcomes. If the awaited promise fulfils, the expression evaluates to the fulfilment value; if it rejects, `await` throws the rejection reason right there, exactly as if you had written `throw reason` on that line. Because the failure becomes a real exception at a known point in the source, ordinary `try`/`catch`/`finally` works again — that is the main ergonomic win of async/await over `.then` chains, where the continuation runs on a later, empty stack that no surrounding `try` can guard. Two details I would call out: the catch binding holds the raw rejection reason, so it can be a string, `undefined`, or a response object rather than an `Error`, and only the awaits lexically inside the `try` are protected. An async callee that throws synchronously before its first await is no different — that throw is captured and turned into a rejection, so the caller still sees it at the `await`.
code
javascript · 16 linesfunction fetchUser(id) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error(`no user ${id}`)), 10));
}
async function load(id) {
try {
const user = await fetchUser(id);
return user.name;
} catch (err) {
console.log('caught:', err.message); // caught: no user 7
return null;
}
}
load(7).then(v => console.log('resolved with', v)); // resolved with nullgo deeper
Be ready to say plainly that await turns a rejected promise into a thrown error at that line, so a normal try/catch around the await handles it. Show the shape in code without hesitating.
Explain the two resumption paths of await and that the catch binding is the raw rejection reason of any type. Point out that a throw before the callee's first await still reaches you as a rejection.
Demonstrate judgment about handler scope: which awaits a try actually protects, why swallowing an error silently converts failure into a fulfilled promise, and how you normalise unknown rejection reasons at a module boundary.
Own the error-channel contract across a codebase: whether async APIs may reject with non-Error values, where reasons get normalised and wrapped, and how that choice affects logging, alerting, and every caller's handler.
## The problem `await` solves Before async/await, asynchronous failures were structurally uncatchable by `try`/`catch`. A callback or a `.then` continuation runs on a later turn of the event loop, on a fresh call stack. By then the `try` block that started the work has already exited, so there is no live handler frame for an exception to unwind into. Wrapping `fetchUser().then(handle)` in a `try` catches only errors thrown while *starting* the work, never the failure the work reports later. ## What `await` actually does `await expr` suspends the async function until `expr` settles, then resumes it in one of exactly two ways: - The promise **fulfils** → the `await` expression evaluates to the fulfilment value and execution continues on the next line. - The promise **rejects** → the `await` expression **throws** the rejection reason at that point, as if `throw reason` appeared on that line. That second rule is the whole mechanism. Rejection is converted back into an ordinary exception at a specific, visible point in the source, so the language's ordinary exception machinery applies again. ```js async function load(id) { try { const user = await fetchUser(id); // rejection throws here return user.name; } catch (err) { console.error('load failed', err); return null; } } ``` The function returns a promise either way: it fulfils with `user.name` on the happy path, and fulfils with `null` on the failure path, because the `catch` handled the error and returned a value. Handling an error inside an async function turns a rejection into a fulfilment unless you rethrow. ## The catch binding holds a reason, not necessarily an Error `throw` accepts any value in JavaScript, and so does `Promise.reject`. The catch parameter is bound to whatever the promise rejected with. A handler that immediately reads `err.message` silently produces `undefined` for a string rejection and throws a `TypeError` for a `null` one. Robust handlers narrow first: ```js catch (err) { const message = err instanceof Error ? err.message : String(err); logger.warn(message); } ``` This matters more with async code than with synchronous code, because rejection reasons often cross a library boundary you do not control. ## Only the awaits inside the block are protected A `try` block guards the awaits that are lexically inside it, and nothing else. If a promise is created inside the `try` but awaited after it, the rejection surfaces at the later `await`, outside the handler. If a promise is created and never awaited, nothing ever throws into that block at all, because the call that produced it returned normally. The practical rule is that the `try` should bracket the `await`, not merely the call that starts the work. ## A synchronous throw inside the callee still arrives as a rejection An async function never throws synchronously to its caller. If `fetchUser` validates its argument and throws on its very first line, before any `await`, the thrown value does not propagate up the caller's stack — it is captured and used to reject the promise the call returned. From the caller's side, an argument-validation bug and a network failure surface identically, at the `await`: ```js async function fetchUser(id) { if (!id) throw new TypeError('id required'); // becomes a rejection return api.get(`/users/${id}`); } ``` That uniformity is deliberate: callers of an async function need exactly one error channel, the returned promise, rather than two. ## Where the catch clause actually runs When an async function suspends at `await`, its remaining body is registered as a continuation on the awaited promise and resumed as a promise job. So the `catch` clause executes on a later turn, on a call stack that no longer contains the caller that invoked the async function. Two consequences are worth naming in an interview. First, the resumed code still sees the same variables — resumption restores the function's environment, so closures and locals are intact. Second, the caught error's stack trace was captured where the error was *created*, which may be deep inside a library; the frames that were live where you wrote the `await` are only visible if the engine records async stack traces, which V8 does for awaited frames but not for every promise shape. ## try/catch versus a `.catch` handler Both catch the same failures. `try`/`catch` protects a region of code and reads sequentially, which is why it dominates in async functions; `.catch` attaches to a single promise and is the only option outside an async function. What you should not conclude is that `try`/`catch` gained new powers — it did not. `await` did the work by moving the failure back onto the throwing path.
- If the catch block handles the error and returns a value, what does the async function's returned promise do?It fulfils with that value. Catching an error inside an async function consumes it, so the promise the function returned resolves normally — callers see success, not failure. If the caller must know the operation failed, the catch has to rethrow (or return a rejected promise) rather than return a fallback.
- Why can't a try/catch placed around a plain `.then` chain catch the asynchronous failure?Because only the synchronous part runs inside the `try`: creating the promise and registering the callback. The callback runs later on a fresh stack, after the `try` block has exited, so there is no live handler for it to unwind into. `await` fixes this by throwing the reason inside the still-live function body when it resumes.
- What happens if the rejection reason is not an Error object?The catch binding is that value verbatim — a string, a number, `undefined`, an object. Nothing normalises it. So `err.message` is `undefined` for a string rejection and reading properties of a `null` rejection throws a TypeError inside your handler. Guard with `err instanceof Error` before assuming Error shape.
saying these in an interview costs you the question
- Says try/catch works because async/await makes code synchronous
- Assumes the caught value is always an Error instance
- Thinks an async function can throw synchronously to its caller
- Believes try/catch around a .then chain catches the rejection
- Claims await blocks the thread while waiting