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?
answer
- the value is the same either way
- frame lifetime is what changes
- one form leaves the block early
- cleanup timing gives it away
- traces keep the hop only one way
basics
~20 sBoth 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.
solid answer
~50 sFor a plain body the two are observationally equivalent in outcome: `return p` resolves the async function's promise with `p`, and because `p` is a thenable, the outer promise adopts it and settles with the same value or reason `return await p` would give. Nesting never happens either way. The difference appears when the `return` is inside a `try` block. With `return p` the function is finished the moment it returns — the frame leaves the `try`, so a later rejection of `p` is not caught by the local `catch`, and a `finally` cleanup runs immediately, before `p` has settled. With `return await p`, the function stays suspended inside the `try`, so a rejection surfaces as a throw the local `catch` handles, and `finally` runs only after settlement. In V8-based engines `return await` also keeps the frame on the reconstructed async stack, giving a more useful trace.
code
javascript · 17 linesconst failing = () => Promise.reject(new Error('boom'));
async function handoff() {
try { return failing(); } // frame leaves try before rejection
catch { return 'caught'; }
}
async function stayInside() {
try { return await failing(); } // rejection arrives inside try
catch { return 'caught'; }
}
handoff().then(
v => console.log('handoff fulfilled:', v),
e => console.log('handoff rejected:', e.message) // rejected: boom
);
stayInside().then(v => console.log('stayInside:', v)); // stayInside: caughtgo deeper
Know that returning a promise from an async function is fine and never produces a promise of a promise, so return await is not needed just to unwrap the value.
Explain that the settled value is identical and that the real difference is whether the frame stays suspended inside the surrounding block, which is what makes a local catch reachable.
Diagnose it from symptoms: a catch that never fires, or cleanup running while the operation is still in flight. Give a concrete rule for when the await belongs, and mention the effect on async stack traces.
Turn it into a convention: decide whether the codebase always writes return await for uniform error containment and traces, or optimises the tail case, and make sure lint configuration matches that decision rather than the outdated redundancy advice.
## Same value, different frame lifetime Both forms produce the same settled value, because settling with a thenable triggers adoption: ```js async function a() { return Promise.resolve(1); } async function b() { return await Promise.resolve(1); } // a() and b() both fulfil with 1 — never with a promise ``` What differs is *when the function's own frame goes away*. `return p` completes the function immediately; the returned promise's fate is delegated to `p`, and the frame is gone. `return await p` keeps the frame alive and suspended until `p` settles, and only then returns. ## The case that actually matters: inside try ```js async function withoutAwait() { try { return Promise.reject(new Error('boom')); } catch { return 'caught'; } } async function withAwait() { try { return await Promise.reject(new Error('boom')); } catch { return 'caught'; } } withoutAwait().then( v => console.log('a fulfilled', v), e => console.log('a rejected:', e.message) // a rejected: boom ); withAwait().then(v => console.log('b fulfilled', v)); // b fulfilled caught ``` In `withoutAwait` the function has already returned by the time the promise rejects, so control is no longer anywhere near the `catch`; the rejection flows straight out to the caller. In `withAwait` the rejection is delivered *into* the suspended frame as a throw at the `await`, still lexically inside the `try`, so the local handler runs. This is exactly the bug behind "my try/catch does not catch anything, and I do have an await somewhere in the function". ## finally timing The same frame-lifetime rule governs cleanup ordering: ```js const slow = new Promise(r => setTimeout(() => r('value'), 50)); async function eager() { try { return slow; } finally { console.log('cleanup ran'); } // logs immediately } async function patient() { try { return await slow; } finally { console.log('cleanup ran'); } // logs after 50 ms } ``` If the `finally` releases something the pending operation still needs — a connection, a lock, a temporary file, an abort controller — the eager form releases it while the work is still in flight. That is a real production defect, not a style question. ## Stack traces V8 reconstructs async stack traces from the chain of suspended frames. `return await p` leaves this function on that chain, so a rejection originating deep inside `p` reports a trace that includes your function. `return p` removes the frame first, and the trace typically loses that hop. On a large codebase this is the difference between a stack that names the failing operation and one that stops at a generic helper. ## Cost, and the lint history `return await` does add internal scheduling steps compared with plain `return`, since the value is unwrapped once for the await and the result then resolves the outer promise. In practice that cost is negligible next to the I/O being awaited. Older style guidance — the `no-return-await` lint rule — treated `return await` as pure redundancy; that advice has since been walked back precisely because of the try-block and stack-trace behaviour, and the rule was deprecated. ## A usable rule of thumb - Inside `try`/`catch`/`finally`, or anywhere the surrounding block owns a resource: **use `return await`**, so the function stays put until the work is done. - In a plain tail position with no surrounding block and no interest in stack quality: `return p` is fine and marginally leaner. - Never justify `return await` by claiming it prevents a nested promise. It does not, because that nesting never existed. ## What interviewers listen for A weak answer says one form is always redundant. A strong answer separates the *value* (identical) from the *frame lifetime* (different), then names the two observable consequences — local catch and finally ordering — and the diagnostic one, stack traces.
- Is `return await p` ever wrong to write?It is never incorrect, only occasionally superfluous: in a plain tail position with no surrounding try or resource ownership, it adds a suspension step for no observable gain. Given how cheap that is next to real I/O, many teams standardise on always writing it rather than asking reviewers to reason about the surrounding block each time.
- Why did the advice to strip `return await` fall out of favour?Because it was based only on the value being identical, and ignored frame lifetime. Removing the await silently breaks a local catch, moves `finally` cleanup ahead of the work it protects, and shortens async stack traces. The `no-return-await` lint rule that encoded the old advice was deprecated for exactly those reasons.
- Does using `return await` change what the caller ultimately receives?No. In both forms the caller gets one promise settling with the same value or the same rejection reason — unless the function's own `catch` intercepts, which only `return await` makes possible. There is no extra promise layer to unwrap in either case, since adoption already flattens thenables.
saying these in an interview costs you the question
- Says return await prevents a nested promise
- Claims the two forms are always interchangeable
- Thinks the local catch fires either way
- Assumes finally waits for the returned promise regardless
- Says removing await changes the fulfilled value