Node.js Async Await

Async/await lets you write asynchronous Node.js code that reads like ordinary synchronous code, while still running on top of promises under the hood.

The async Keyword

Marking a function with the async keyword does two things: it makes the function always return a promise, and it allows you to use the await keyword inside its body. Even if you write 'return 5' inside an async function, the caller receives a promise that resolves to 5, not the number itself.

A basic async function

async function getGreeting() {
  return 'Hello from an async function';
}

getGreeting().then((message) => console.log(message));
// Logs: Hello from an async function

The await Keyword

Inside an async function, await pauses execution of that function until the promise it's given settles. If the promise resolves, await evaluates to the resolved value. If the promise rejects, await throws the rejection reason, which is why try/catch is the standard way to handle errors in async/await code.

Reading a file with await and try/catch

const fs = require('fs/promises');

async function loadConfig() {
  try {
    const raw = await fs.readFile('config.json', 'utf8');
    const config = JSON.parse(raw);
    console.log('Environment:', config.environment);
    return config;
  } catch (err) {
    console.error('Could not load config:', err.message);
    throw err; // re-throw so callers know loading failed
  }
}

loadConfig();
Note: await only pauses the current async function, not the entire program. Other code — including other requests in an HTTP server — keeps running on the event loop while the function is paused.

Sequential Awaits

When you await one call after another, each one waits for the previous to finish before starting. This is correct when each step genuinely depends on the result of the last, but it's a common performance mistake when the operations are actually independent.

Sequential awaits (slower, one after another)

const fs = require('fs/promises');

async function readSequential() {
  console.time('sequential');
  const a = await fs.readFile('a.txt', 'utf8');
  const b = await fs.readFile('b.txt', 'utf8');
  const c = await fs.readFile('c.txt', 'utf8');
  console.timeEnd('sequential');
  return [a, b, c];
}

readSequential();

Parallel Awaits

To run independent async operations concurrently, start all the promises first without awaiting them individually, then await them together with Promise.all. This lets Node kick off all the I/O operations immediately and wait for the slowest one, rather than summing every operation's duration.

Parallel awaits (faster, all at once)

const fs = require('fs/promises');

async function readParallel() {
  console.time('parallel');
  const [a, b, c] = await Promise.all([
    fs.readFile('a.txt', 'utf8'),
    fs.readFile('b.txt', 'utf8'),
    fs.readFile('c.txt', 'utf8')
  ]);
  console.timeEnd('parallel');
  return [a, b, c];
}

readParallel();
PatternWhen to useTotal time (roughly)
Sequential awaitEach step depends on the previous resultSum of every step's duration
Promise.all + awaitSteps are independent of each otherDuration of the slowest step
Note: A good rule of thumb: if the second operation doesn't use any value produced by the first, you almost always want them in parallel.

Async/await doesn't replace promises — it's built entirely on top of them, so anything returning a 'thenable' can be awaited, including your own hand-written Promise-based functions.

Exercise: Node.js Async/Await

What does an `async` function always return, regardless of its body?