This post explains a quiz originally shared as a LinkedIn poll.
🔹 The Question
console.log('start');
async function task() {
try {
return 'data';
} finally {
await Promise.resolve();
return 'cleanup';
}
}
task().then((v) => console.log('then', v));
queueMicrotask(() => console.log('micro'));
console.log('end');
Hint: In async functions, a return in finally can override the try return. Also, note when the continuation after await is enqueued relative to queueMicrotask.
🔹 Solution
Correct output / order: start, end, micro, then cleanup
đź§ How this works
This combines two behaviors that frequently surprise even experienced developers:
-
finallycan overridereturn: in both sync and async functions, areturninfinallywins over areturnintry. Here,task()resolves to'cleanup', not'data'. -
Microtask ordering is “first queued, first run”: the
await Promise.resolve()schedules the async function’s continuation as a microtask. That microtask is queued during the call totask(), before you later callqueueMicrotask(...).
So you get a deterministic ordering even though everything is “microtasks”.
🔍 Line-by-line explanation
-
console.log('start')runs synchronously → printsstart. -
task().then(...)callstask()immediately. - Inside
task():-
try { return 'data' }is evaluated, but execution must still runfinally. - In
finally,await Promise.resolve()is hit:- This schedules the continuation of
task()as a microtask (call it R). -
task()returns a pending promise to the caller for now.
- This schedules the continuation of
-
- Back in the outer script:
-
queueMicrotask(() => console.log('micro'))queues another microtask (call it M) after R. -
console.log('end')runs synchronously → printsend.
-
- Microtasks flush:
-
R runs first (it was queued first): the async function resumes after
awaitand executesreturn 'cleanup'.- That resolves the promise returned by
task()with'cleanup'. - Resolving it queues the
.then(...)handler as a new microtask (call it T) at the end of the queue.
- That resolves the promise returned by
-
M runs next → prints
micro. -
T runs last → prints
then cleanup.
-
R runs first (it was queued first): the async function resumes after
The non-obvious part: many people assume the queueMicrotask callback will run before “the rest of the async function”, because they picture the await continuation as “later”. But it was queued earlier in the same tick.
🔹 Key Takeaways
- In JavaScript,
returninfinallyoverridesreturnintry(and can also override athrow). -
awaitsplits execution: the continuation is scheduled as a microtask. - Microtasks are ordered: work queued earlier in the tick runs first, even if it “feels” like it came from deeper async code.
- Treat
returninsidefinallyas a code smell unless you’re being very deliberate.
Top comments (0)