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?
answer
- the call signature changes, not just the body
- one fixed wrapper on every call
- two outcomes, two settle paths
- nesting never happens on return
- even a pre-await throw stays inside
basics
~20 sAn 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 sMarking 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
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.
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.
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.
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