skip to content

Async/Await

The syntax layer over promises: async functions, await, and the error handling that comes with them. Interviewers care that you can still see the promises underneath, because that is what explains accidental serialization and lost rejections.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

18

In JavaScript, what does calling an `async` function return, and how do a plain `return 42`, a `throw`, and a `return somePromise` inside its body each show up in that returned value?

level: juniorimportance: must knowfreq 85%

answer

  1. the call signature changes, not just the body
  2. one fixed wrapper on every call
  3. two outcomes, two settle paths
  4. nesting never happens on return
  5. even a pre-await throw stays inside

basics

~20 s

An async function always returns a promise, never a raw value: return 42 fulfils that promise with 42, throw rejects it with the thrown error, and returning a promise makes the outer promise adopt that promise's eventual outcome.

solid answer

~40 s

Marking a function `async` changes its call signature: it always returns a promise, whatever the body does. `return 42` fulfils that promise with `42`. `throw new Error('boom')` rejects it with that error — nothing is thrown synchronously at the call site, even if the throw happens before any `await`. `return somePromise` does not give you a promise of a promise: the outer promise resolves with the inner one, and because the inner one is a thenable, the outer adopts it and settles with its value or reason. A function marked `async` with no `await` in it still obeys all of this — the body runs straight through, then the returned promise fulfils. The promise is always a plain native `Promise`, not a subclass.

go deeper

for a junior

Be able to say plainly that calling an async function hands you a promise every time, that return fulfils it and throw rejects it, and that you need await or .then to reach the value.

for a middle

Explain the resolution step: returning a thenable makes the outer promise adopt it rather than nest it, and a throw is captured into a rejection of a promise that already exists before the body runs.

for a senior

Show that this contract is what makes async functions safely composable in real code, and point at the failure it causes in practice — call sites that swallow rejections because they never awaited or attached a handler.

for a principal

Talk about it as an API design decision: making a function async changes its published error contract for every caller, so uniform promise-returning surfaces are worth choosing deliberately rather than sprinkling async where it happens to be convenient.

## The single guarantee The `async` keyword is a promise-shaped contract on the function's *return type*. Before the body starts executing, the engine creates a promise using the built-in `Promise` constructor. That promise is what the caller receives; the body's eventual outcome only decides how it settles. So the type of `f()` is fixed at the declaration site — you can always `.then()` it, always `await` it, and you can never get a bare value out of it synchronously. ```js async function f() { return 42; } const r = f(); console.log(r); // Promise { 42 } (already fulfilled) console.log(r instanceof Promise); // true r.then(v => console.log(v)); // 42 ``` ## return fulfils A `return v` inside the body fulfils the returned promise with `v`. `return` with no value fulfils it with `undefined`, and so does falling off the end of the body. That is why an async function that only performs side effects still gives the caller something meaningful to await — a signal that the work finished. ## throw rejects A `throw` anywhere in the body — including in the synchronous stretch before the first `await` — rejects the returned promise with the thrown value. It does **not** surface as a synchronous exception at the call site. This is the trap juniors hit most: ```js async function boom() { throw new Error('nope'); } try { boom(); // no exception here } catch (e) { console.log('never runs'); } boom().catch(e => console.log('caught:', e.message)); // caught: nope ``` The rejection is a value travelling through the promise, not a stack unwind through the caller. The caller has to opt into seeing it, by awaiting the call or attaching a rejection handler. ## Returning a promise: adoption, not nesting The most commonly misstated part is `return somePromise`. People expect `Promise<Promise<T>>`. You never get that. Settling a promise runs the promise *resolution* procedure: if the resolution value is a **thenable** (any object with a callable `then` method — every promise qualifies), the outer promise does not fulfil with it. It subscribes to it and adopts its fate: fulfil with the inner value, or reject with the inner reason. ```js async function g() { return Promise.resolve('x'); } g().then(v => console.log(v, typeof v)); // x string — not a Promise async function h() { return Promise.reject(new Error('inner')); } h().catch(e => console.log(e.message)); // inner ``` The practical upshot: an async wrapper around a promise-returning call is transparent. `async function get() { return fetchThing(); }` is, from the caller's point of view, the same shape as `fetchThing()` itself. ## async with no await Adding `async` to a function that contains no `await` is not a no-op. The body still runs synchronously from start to finish when you call it — nothing defers it — but the value now arrives wrapped and any throw becomes a rejection. That is sometimes exactly what you want (a uniform promise-returning interface across a set of functions, some cheap and some not), and sometimes an accidental change in a function's error contract. ## The promise is always the built-in one Async functions construct their result with the intrinsic `Promise`. Defining an async method inside a class that `extends Promise` does not make the method hand back an instance of your subclass. If you need subclass behaviour, wrap the call explicitly. ## Why interviewers ask this It separates people who think `async` is a magic word that "makes things asynchronous" from people who know it is a return-value transform plus a suspension mechanism. Almost every downstream async bug — an unhandled rejection, a `try/catch` that catches nothing, a value logged as `Promise { <pending> }` — traces back to a candidate not holding this contract firmly in mind.

  • If an async function throws before it ever reaches an `await`, does the caller see a synchronous exception?
    No. The synchronous prefix runs on the call, but a throw in it is captured and turned into a rejection of the already-created returned promise. A plain `try { f() } catch {}` around the call sees nothing. The caller must `await f()` or attach `.catch()` to observe it.
  • Does marking a function `async` change anything if its body contains no `await` at all?
    Yes. The body still runs synchronously to completion on the call, but the return value is now wrapped in a promise and any throw becomes a rejection instead of a synchronous exception. That is a real change to the function's contract, even though nothing was deferred.
  • Can an async function return an instance of a Promise subclass, for example inside a class that extends Promise?
    No. Async functions build their result with the intrinsic `Promise` constructor; there is no constructor lookup on the surrounding class. If you need a subclass instance, construct it explicitly around the async call, for example `MyPromise.resolve(f())`.

saying these in an interview costs you the question

  • Says an async function returns the value when there is no await
  • Thinks returning a promise gives a promise of a promise
  • Believes a throw inside async reaches the caller synchronously
  • Assumes async only matters if the body uses await
  • Says the caller gets undefined until the body finishes

context

open as a page

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?

level: juniorimportance: must knowfreq 78%

basics

~20 s

await 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.

open as a page

In JavaScript, a function loops over 10 user IDs and does `const user = await fetchUser(id)` inside the loop body, where each call takes about 200 ms. How long does the loop take overall, why, and how would you make the ten requests run concurrently?

level: juniorimportance: must knowfreq 80%

basics

~20 s

Awaiting inside the loop serializes the requests, so ten 200 ms calls take about 2 seconds. Start all ten promises first and await them together with Promise.all, and the whole batch takes roughly 200 ms.

open as a page

When execution inside an `async` function reaches its first `await`, what does the caller of that function receive at that moment, and is anything blocked while the awaited value is still pending?

level: middleimportance: must knowfreq 75%

basics

~20 s

The caller receives a pending promise straight away. The async function's frame is suspended, not blocked: its locals are parked, control returns to the caller, and the rest of the program keeps running until the awaited value settles and the function resumes.

open as a page

Rewrite this async function as an equivalent promise chain and say what the rewrite reveals about `await`: `async function load(id) { const user = await fetchUser(id); const posts = await fetchPosts(user.id); return { user, posts }; }`

level: middleimportance: must knowfreq 70%

basics

~10 s

Each await splits the function in two: the awaited expression becomes the promise you attach to, and everything after it becomes the callback. So load becomes fetchUser(id).then(user => fetchPosts(user.id).then(posts => ({ user, posts }))).

open as a page

Inside a JavaScript async function you write `try { doWork(); } catch (err) { ... }`, where `doWork` is an async function that rejects. Why does the catch block never run, and where does that error end up?

level: middleimportance: must knowfreq 72%

basics

~20 s

Calling an async function returns a promise immediately without throwing, so the try block completes normally and its frame is gone before the promise rejects. try/catch is a synchronous, stack-based mechanism, so the rejection escapes it entirely and is left unhandled.

open as a page

In JavaScript, does `const a = await getA(); const b = await getB();` take the same total time as `const pa = getA(); const pb = getB(); const a = await pa; const b = await pb;`? Explain what makes them differ.

level: middleimportance: must knowfreq 66%

basics

~20 s

No. Calling getA() and getB() starts both operations immediately, so the second version overlaps them and costs about the slower one. Awaiting each call inline starts the second only after the first has settled, so it costs the sum.

open as a page

What does adding a top-level `await` to an ES module do to the modules that import it, and to unrelated modules elsewhere in the same graph?

level: middleimportance: must knowfreq 58%

basics

~20 s

The module becomes an asynchronous module: its evaluation now completes with a promise. Every module that imports it, directly or transitively, has its own body deferred until that promise settles. Modules that do not depend on it still evaluate normally.

open as a page

In JavaScript, what happens when you `await` something that is not a promise — for example `await 42`, or `await` on a plain object that merely has a `then` method?

level: middleimportance: should knowfreq 50%

basics

~20 s

await resolves whatever it is given. A non-thenable such as 42 simply becomes the result, and any object with a callable then method is treated as a promise: its then is called and await produces whatever that method resolves with. Either way the function suspends first.

open as a page

In a JavaScript async function, does a `finally` block still run when an awaited promise inside the `try` rejects, and what changes if the cleanup code in that `finally` itself needs an `await`?

level: middleimportance: should knowfreq 56%

basics

~20 s

Yes — await turns the rejection into a normal throw, so finally runs before the error propagates out. If the cleanup is itself asynchronous you must await it inside the finally, otherwise the function's promise settles while cleanup is still pending and any cleanup failure floats unhandled.

open as a page

Inside a try block in a JavaScript async function you write `const user = await getUser(id).catch(() => null);`. If `getUser` rejects, what is `user`, and does the surrounding catch block run?

level: middleimportance: should knowfreq 44%

basics

~20 s

user is null and the surrounding catch never runs. The .catch handler returns a new promise that fulfils with the handler's return value, so by the time await sees it there is no rejection left to throw.

open as a page

In JavaScript, `const results = items.map(async (item) => save(item));` gives an array of pending promises rather than saved results, and `items.forEach(async (item) => { await save(item); });` returns before anything has been saved. Explain both behaviours and give the correct way to save every item.

level: middleimportance: should knowfreq 55%

basics

~20 s

An async callback always returns a promise, so map collects promises and you must await them with Promise.all. forEach discards whatever its callback returns, so nothing waits for the saves and any rejection becomes unhandled. Use await Promise.all(items.map(...)).

open as a page

A top-level `await` in an ES module rejects. What happens to that module and to the modules that import it, and can an importer catch the error with try/catch?

level: middleimportance: should knowfreq 32%

basics

~20 s

The module's evaluation fails with that error, and the failure propagates to every module importing it, which never runs. An importer cannot wrap a static import in try/catch, so the error must be handled inside the awaiting module or by whatever started the graph.

open as a page

Inside an async function, what is the difference between `return somePromise` and `return await somePromise`, and in which situation does the choice actually change observable behaviour?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Both settle the outer promise with the same value, because returning a promise adopts it. The difference shows inside try/catch/finally: return await keeps the frame suspended in the block, so a rejection is caught locally and finally runs after settling; plain return hands the promise off and leaves first.

open as a page

You catch an error inside an async JavaScript function so you can log it, but the caller still needs to know the operation failed. What are your options for passing the failure on, and what does each do to the promise that async function returns?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Catching without rethrowing makes the async function fulfil, so the caller sees success. To propagate, either rethrow the original error, throw a new error carrying the original as its cause, or return a rejected promise — all three reject the returned promise.

open as a page

A reviewer tells you to replace every `await` inside a loop with a `Promise.all` over the whole collection. When is that refactor wrong or dangerous, and how do you decide case by case?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Only when the iterations are independent. The refactor breaks genuine data dependencies and order-sensitive side effects, and an unbounded fan-out over a large collection can exhaust connections or trip rate limits — cases where sequential or capped execution is the correct shape.

open as a page

A shared `db.mjs` module in your service runs `const pool = await connectToDatabase()` at the top level. Startup has become slow and the service sometimes hangs on boot. Explain why, and how you would restructure it.

level: seniorimportance: should knowfreq 38%

basics

~20 s

Import-time connection makes every module that imports db.mjs wait for the network, with no timeout, retry, or ordering control, and an unreachable database stalls boot forever. Move the connection into a lazily-invoked, memoised async function that callers await at the point of use.

open as a page