Node.js Async Programming

Asynchronous programming is at the heart of Node.js. Reading files, querying databases and calling APIs all take time, and Node never waits idly for them. Instead it starts the task and continues; the result arrives later through a callback, a promise, or async/await.

Synchronous vs Asynchronous

Even with a 0 ms delay, the timer callback waits until the current code finishes. The event loop runs queued callbacks only when the call stack is empty.

console.log('1. start');
setTimeout(() => console.log('3. timer done'), 0);
console.log('2. end');
1. start
2. end
3. timer done

Callbacks

Older Node APIs pass a callback as the last argument. By convention the first callback argument is an error (error-first callbacks). Nesting many callbacks leads to hard-to-read "callback hell".

import fs from 'node:fs';

fs.readFile('note.txt', 'utf8', (err, data) => {
  if (err) return console.error('Failed:', err.message);
  console.log('Content:', data);
});
Content: Remember to push the code.

Promises and async/await

Promises represent a future value. async/await lets you write promise-based code that reads like normal top-to-bottom code. Most Node APIs have promise versions, such as node:fs/promises.

import { readFile } from 'node:fs/promises';

async function showNote() {
  try {
    const data = await readFile('note.txt', 'utf8');
    console.log('Content:', data);
  } catch (err) {
    console.error('Failed:', err.message);
  }
}

await showNote();
Content: Remember to push the code.

Running Tasks in Parallel

await inside a loop runs tasks one after another. When tasks are independent, start them all and wait with Promise.all.

const sleep = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));

console.time('parallel');
const results = await Promise.all([
  sleep(300, 'users'),
  sleep(200, 'orders'),
  sleep(100, 'stats'),
]);
console.log(results);
console.timeEnd('parallel');
[ 'users', 'orders', 'stats' ]
parallel: 302.4ms

Promise Helpers

  • Promise.all — waits for all; rejects as soon as one fails.
  • Promise.allSettled — waits for all and reports each success or failure.
  • Promise.race — settles with the first one to finish.
  • Promise.any — resolves with the first success.
  • util.promisify — turns an old callback function into a promise function.

Unhandled Rejections

If a promise rejects and nothing catches it, Node prints an error and exits the process (since Node 15). Always await promises inside try/catch, or attach .catch(), so errors are handled deliberately.

Frequently asked questions

What is the event loop in Node.js?

The event loop is the mechanism that lets single-threaded Node run asynchronous code. It repeatedly takes completed operations and timers from queues and runs their callbacks when the call stack is free.

Is async/await better than promises?

async/await is built on promises; it is just nicer syntax. Use await for readable sequential code and combine it with Promise.all for parallel work.

Can I use await at the top level in Node.js?

Yes, in ES modules (files with "type": "module" or .mjs). In CommonJS you must wrap the code in an async function.