Asynchronous Programming
Callbacks
JavaScript
Debugging
Software Development

Last callback not being called using async

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

When the "last callback" in asynchronous JavaScript never runs, the root cause is usually not randomness in the event loop. It is almost always a control-flow bug: a promise chain that never resolves, a callback path that is skipped on one branch, or a thrown error that stops execution before the final callback is reached.

The First Rule: Every Async Path Must Finish

If one branch neither resolves nor rejects, the code waiting for completion has nothing to continue from.

javascript
1function doWork(flag, done) {
2  if (flag) {
3    setTimeout(() => done(null, "ok"), 100);
4    return;
5  }
6
7  // Bug: no callback here
8}
9
10doWork(false, (err, result) => {
11  console.log("final callback", err, result);
12});

In this example, the final callback is not "mysteriously ignored." The code simply never called it when flag was false.

Mixing Callbacks and async Functions Causes Confusion

Another common issue is mixing callback-based APIs with async and await in a way that leaves one completion style hanging.

javascript
1async function handler(done) {
2  await Promise.resolve("step finished");
3  done();
4}

This is fine if the caller truly expects a callback. But if the surrounding framework expects you to return a promise instead of calling done, or expects done without any returned promise semantics, the control flow becomes muddy quickly.

Choose one pattern for a function:

  • callback-based completion
  • promise-based completion

Do not casually combine both unless the framework explicitly supports it.

Errors Can Stop the Final Callback Before It Runs

If an exception is thrown inside async code and never handled, later steps may never execute.

javascript
1async function processItems(items, done) {
2  try {
3    for (const item of items) {
4      await saveItem(item);
5    }
6
7    done(null, "finished");
8  } catch (error) {
9    done(error);
10  }
11}

Without the try and catch, a rejection from saveItem could short-circuit the rest of the function, making it look as though the final callback was skipped for no reason.

Array Helpers Are a Frequent Trap

Developers often use forEach with async callbacks and expect the surrounding code to wait.

javascript
1items.forEach(async (item) => {
2  await saveItem(item);
3});
4
5done();

Here done() runs immediately, before any of the saveItem calls finish. The opposite bug can also happen if the final callback is placed somewhere that is never reached after asynchronous fan-out.

If you need to wait for everything, use Promise.all or a for...of loop.

javascript
await Promise.all(items.map((item) => saveItem(item)));
done();

Debug the Completion Boundary

When the last callback is missing, log every place that should lead to completion:

javascript
1console.log("start");
2console.log("before await");
3console.log("after await");
4console.log("before done");

The goal is to find the exact branch where the flow stops. Once you know that, the bug is usually one of:

  • callback never invoked
  • promise never resolved
  • error thrown before final step
  • function returned too early

Common Pitfalls

The biggest pitfall is assuming every branch eventually calls the callback. In callback-heavy code, one missing return done(...) on an error path is enough to make the whole function hang.

Another common mistake is mixing promise and callback styles in the same function without defining which one actually signals completion. That often leads to double-calls, missed calls, or code that completes too early.

Developers also misuse forEach with async functions and then blame the event loop, when the real problem is that forEach does not wait for asynchronous work.

Summary

  • A missing final callback usually means one async path never completed correctly.
  • Make sure every branch either calls the callback or resolves or rejects the promise.
  • Do not mix callback and promise styles casually in the same function.
  • Handle errors explicitly so they do not silently skip the final completion step.
  • If async loops are involved, prefer for...of or Promise.all over forEach.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.