Node.JS Async / Await Dealing With Callbacks?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
async and await make asynchronous JavaScript easier to read, but many Node.js APIs still use callbacks. The practical solution is to convert callback-based functions into promises, then compose them with await. This article shows reliable patterns for bridging both styles without hidden error handling bugs.
Understand Error-First Callback Shape
Classic Node callbacks usually follow the error, result convention.
To use await, wrap this behavior in a promise.
Convert Callbacks With util.promisify
Node provides util.promisify for standard callback signatures.
This is usually the quickest migration path for built-in modules.
Manual Promise Wrapping for Custom APIs
If the callback API is non-standard, wrap it manually.
Manual wrapping gives full control over argument mapping and validation.
Compose Sequential and Parallel Workflows
Once functions return promises, composition is straightforward.
Use sequential mode when calls depend on previous results. Use parallel mode for independent calls where throughput matters.
Avoid Callback and Promise Mixing in One API
Pick one async contract at module boundaries. If a function both returns a promise and expects a callback, call sites become ambiguous.
A clean migration strategy is:
- Wrap legacy callbacks in adapter functions.
- Export promise-first APIs from new modules.
- Keep callback style only at integration edges until migrated.
This keeps error handling predictable and avoids duplicate completion bugs.
Add Timeouts and Cancellation
Real applications need cancellation boundaries so hanging calls do not block request pipelines forever. For promise-based APIs, combine wrappers with timeout logic.
When callback APIs support cancellation handles, expose them in your adapter design so higher layers can abort work intentionally.
Testing Promise Adapters
Write focused tests for the adapter layer to ensure callback errors map to rejected promises correctly.
Small adapter tests prevent subtle async regressions during refactors.
Common Pitfalls
A common bug is forgetting to return after reject in manual wrappers, which can lead to multiple resolution attempts. Another issue is using await inside Array.prototype.forEach, which does not wait as many developers expect. Prefer for...of or Promise.all with map. Teams also sometimes swallow errors in inner callbacks and then wonder why outer try blocks do not catch them. Ensure callback errors are converted to promise rejections. Finally, avoid promisifying the same function repeatedly inside hot paths. Create adapters once and reuse them.
Summary
- Bridge callback APIs to promises before using
asyncandawait. - Use
util.promisifyfor standard error-first callbacks. - Manually wrap non-standard callback signatures.
- Choose sequential or parallel composition based on dependency and throughput.
- Keep module APIs consistent to avoid mixed async contracts.

