DEV Community

ValPetal Tech Labs
ValPetal Tech Labs

Posted on

Question of the Day #4 [Talk::Overflow]

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');
Enter fullscreen mode Exit fullscreen mode

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:

  1. finally can override return: in both sync and async functions, a return in finally wins over a return in try. Here, task() resolves to 'cleanup', not 'data'.
  2. 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 to task(), before you later call queueMicrotask(...).

So you get a deterministic ordering even though everything is “microtasks”.

🔍 Line-by-line explanation

  1. console.log('start') runs synchronously → prints start.
  2. task().then(...) calls task() immediately.
  3. Inside task():
    • try { return 'data' } is evaluated, but execution must still run finally.
    • 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.
  4. Back in the outer script:
    • queueMicrotask(() => console.log('micro')) queues another microtask (call it M) after R.
    • console.log('end') runs synchronously → prints end.
  5. Microtasks flush:
    • R runs first (it was queued first): the async function resumes after await and executes return '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.
    • M runs next → prints micro.
    • T runs last → prints then cleanup.

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, return in finally overrides return in try (and can also override a throw).
  • await splits 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 return inside finally as a code smell unless you’re being very deliberate.

Top comments (0)