Back to Node.js tutorials
Intermediate16 min read

Async Patterns & Callbacks

Move from callbacks to Promises and async/await while handling errors and concurrency safely.

Callbacks and Callback Hell

Early Node APIs use callbacks with error-first convention: (err, result) => void. Nested callbacks become hard to read and error handling duplicates at each level.

util.promisify converts callback-style functions to Promise-returning versions for fs and legacy libraries.

Recognize callbacks in older modules but write new code with async/await for clarity.

  • Always handle err as first callback argument
  • Avoid mixing callbacks and Promises in the same function
  • Prefer fs/promises over promisify for built-in fs APIs
import { promisify } from 'node:util';
import { readFile } from 'node:fs';

const readFileAsync = promisify(readFile);
const data = await readFileAsync('file.txt', 'utf8');

Promises and Composition

Promises represent a single async result. Chain with then/catch or use Promise.all for parallel work and Promise.allSettled when partial failure is acceptable.

Race timeouts with Promise.race and manual timer Promises to bound slow dependencies.

Wrap third-party event APIs with new Promise((resolve, reject) => ...) when no Promise interface exists.

  • Return Promises from all async service methods
  • Use Promise.allSettled for bulk operations with independent failures
  • Avoid floating Promises; await or attach catch in entrypoints
const [user, orders] = await Promise.all([
  fetchUser(id),
  fetchOrders(id),
]);

Async/Await Best Practices

async functions return Promises implicitly. await pauses within the function without blocking the event loop globally.

Use try/catch around await sequences for local error handling. For parallel tasks, await Promise.all rather than awaiting inside a loop sequentially unless order matters.

Top-level await works in ES modules for scripts and test files, simplifying bootstrap code.

  • Use for...of with await when steps depend on previous results
  • Use Promise.all with map for independent concurrent tasks
  • Mark async route handlers and always forward errors to Express
async function createOrder(input) {
  try {
    const user = await validateUser(input.userId);
    return await db.orders.insert({ ...input, userId: user.id });
  } catch (err) {
    logger.error(err);
    throw new AppError('Order failed', { cause: err });
  }
}

Get In Touch


Ready to discuss your next project? Drop me a message.