# How to Use Promises in Node.js for Effective Asynchronous Operations

> Master Node.js promises for async operations. Learn to use util.promisify, native fs/promises, and robust error handling with try/catch or .catch. Boost your Node.js development.

- Repository: [Node.js/node](https://github.com/nodejs/node)
- Tags: best-practices
- Published: 2026-02-16

---

**Use `util.promisify()` to convert legacy callbacks, leverage native modules like `fs/promises`, and always handle rejections with `try/catch` or `.catch()` to prevent unhandled rejection warnings.**

Working with **promises in Node.js** requires understanding both the high-level APIs and the underlying architecture of the `nodejs/node` repository. The runtime provides first-class Promise support through utility functions, native promisified modules, and deep integration with cancellation signals. Mastering these patterns ensures your asynchronous code remains reliable, cancellable, and consistent with Node.js core design principles.

## Understanding the Node.js Promise Architecture

Node.js implements its Promise ecosystem in [`lib/internal/util.js`](https://github.com/nodejs/node/blob/main/lib/internal/util.js) and specialized modules like [`lib/internal/fs/promises.js`](https://github.com/nodejs/node/blob/main/lib/internal/fs/promises.js). The architecture centers on three pillars: converting legacy callbacks, providing native Promise-returning APIs, and standardizing error propagation through `handleErrorFromBinding`.

### The util.promisify() Implementation

The `util.promisify()` function, defined in [`lib/internal/util.js`](https://github.com/nodejs/node/blob/main/lib/internal/util.js) (lines 76-135), transforms error-first callback functions into Promise-returning equivalents. It checks for a custom symbol `Symbol.for('nodejs.util.promisify.custom')`, allowing library authors to expose optimized Promise implementations rather than generic wrappers.

### Native Promise Modules

Node.js ships dedicated Promise-based modules to eliminate wrapping overhead:

- **`fs/promises`** – File system operations (`readFile`, `writeFile`, etc.) implemented in [`lib/internal/fs/promises.js`](https://github.com/nodejs/node/blob/main/lib/internal/fs/promises.js)
- **`timers/promises`** – `setTimeout`, `setImmediate`, and `setInterval` with AbortSignal support
- **`dns/promises`** – DNS resolution methods
- **`stream/promises`** – Pipeline and finished helpers

These modules automatically reject with proper error codes and support cancellation via `AbortSignal`.

## Converting Callback APIs to Promises in Node.js

When working with legacy libraries that use error-first callbacks, use `util.promisify()` to standardize your codebase.

```javascript
const { promisify } = require('util');
const fs = require('fs');

// Convert fs.readFile to Promise-based
const readFileAsync = promisify(fs.readFile);

(async () => {
  try {
    const data = await readFileAsync('example.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error('Read failed:', err);
  }
})();

```

*Implementation reference:* [[`lib/internal/util.js`](https://github.com/nodejs/node/blob/main/lib/internal/util.js) – `promisify` core logic](https://github.com/nodejs/node/blob/main/lib/internal/util.js#L76-L135)

## Using Native fs/promises for File Operations

Avoid wrapping `fs` methods manually when native Promise support exists. The `fs/promises` module in [`lib/internal/fs/promises.js`](https://github.com/nodejs/node/blob/main/lib/internal/fs/promises.js) provides optimized implementations that preserve stack traces and handle libuv error codes through `handleErrorFromBinding`.

```javascript
const { readFile, writeFile } = require('node:fs/promises');

async function copyFile(src, dest) {
  const content = await readFile(src);
  await writeFile(dest, content);
  console.log('Copy complete');
}

copyFile('source.txt', 'dest.txt').catch(console.error);

```

*Source:* `fs/promises` API in [[`lib/internal/fs/promises.js`](https://github.com/nodejs/node/blob/main/lib/internal/fs/promises.js)](https://github.com/nodejs/node/blob/main/lib/internal/fs/promises.js#L20-L40).

## Handling Cancellation with AbortSignal

Modern **promises in Node.js** support cancellation through `AbortController`. The `timers/promises` module, documented in [`doc/api/timers.md`](https://github.com/nodejs/node/blob/main/doc/api/timers.md), accepts a `signal` option that rejects the Promise with an `AbortError` when triggered.

```javascript
const { setTimeout } = require('node:timers/promises');
const { AbortController } = require('node:abort-controller');

const ac = new AbortController();

(async () => {
  try {
    const result = await setTimeout(5000, 'completed', { signal: ac.signal });
    console.log(result);
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('Operation cancelled');
    }
  }
})();

// Cancel after 1 second
setTimeout(() => ac.abort(), 1000);

```

*Docs:* Timers Promises API – `setTimeout` options (`signal`) in [[`doc/api/timers.md`](https://github.com/nodejs/node/blob/main/doc/api/timers.md)](https://github.com/nodejs/node/blob/main/doc/api/timers.md#timers-promises-api).

## Managing Multiple Results and Error Propagation

When working with complex asynchronous flows, Node.js provides utilities for handling multiple return values and consistent error handling.

### Custom Promisify Arguments

For callbacks that return multiple arguments (e.g., `callback(err, size, mtime)`), use `customPromisifyArgs` to convert them into a named object. This pattern is tested in [`test/parallel/test-util-promisify.js`](https://github.com/nodejs/node/blob/main/test/parallel/test-util-promisify.js) (lines 83-96).

```javascript
const { promisify, customPromisifyArgs } = require('util');

function getStats(callback) {
  // Simulates callback(err, size, mtime)
  callback(null, 1234, new Date());
}

// Define named properties for the result
getStats[customPromisifyArgs] = ['size', 'modified'];

(async () => {
  const { size, modified } = await promisify(getStats)();
  console.log({ size, modified });
})();

```

*Test reference:* [`test-util-promisify.js`](https://github.com/nodejs/node/blob/main/test-util-promisify.js) lines 83‑96.

### Error Handling Patterns

Node.js normalizes errors through `handleErrorFromBinding` in [`lib/internal/util.js`](https://github.com/nodejs/node/blob/main/lib/internal/util.js) (lines 46-53) to preserve stack traces and expose underlying libuv error codes. Always handle rejections explicitly to prevent `unhandledRejection` warnings.

```javascript
const { readFile } = require('node:fs/promises');

async function safeRead(path) {
  try {
    return await readFile(path, 'utf8');
  } catch (err) {
    // err.code contains the libuv error code (e.g., 'ENOENT')
    console.error(`Error ${err.code}: ${err.message}`);
    throw err; // Re-throw or handle gracefully
  }
}

```

*Implementation reference:* [`handleErrorFromBinding` in lib/internal/util.js](https://github.com/nodejs/node/blob/main/lib/internal/util.js#L46-L53).

### Parallel Execution with Promise.allSettled

When running multiple independent async operations, use `Promise.allSettled` (available since Node.js v12.9) to ensure you capture all results even if some fail.

```javascript
const { readFile } = require('node:fs/promises');

async function readMultiple(files) {
  const results = await Promise.allSettled(
    files.map(f => readFile(f, 'utf8'))
  );
  
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`File ${files[index]}:`, result.value);
    } else {
      console.error(`File ${files[index]} failed:`, result.reason.message);
    }
  });
}

```

## Summary

- **Use native Promise modules** (`fs/promises`, `timers/promises`, etc.) instead of wrapping callbacks manually to ensure consistent error handling and AbortSignal support.
- **Convert legacy callbacks** with `util.promisify()`, checking for `util.promisify.custom` symbols when available for optimized implementations.
- **Handle rejections explicitly** using `try/catch` with `await` or `.catch()` to prevent `unhandledRejection` warnings and leverage Node.js error normalization via `handleErrorFromBinding`.
- **Support cancellation** by passing `AbortSignal` to Promise-based APIs, preventing hanging operations in long-running tasks.
- **Manage complex results** using `customPromisifyArgs` for multi-argument callbacks and `Promise.allSettled` for parallel execution with graceful error handling.

## Frequently Asked Questions

### What is the difference between util.promisify and fs/promises?

`util.promisify` is a utility function in [`lib/internal/util.js`](https://github.com/nodejs/node/blob/main/lib/internal/util.js) that converts any error-first callback function into a Promise-returning function at runtime. In contrast, `fs/promises` (implemented in [`lib/internal/fs/promises.js`](https://github.com/nodejs/node/blob/main/lib/internal/fs/promises.js)) is a native module where file system methods are implemented as Promises from the ground up, offering better performance, native AbortSignal support, and consistent error handling through `handleErrorFromBinding`.

### How does AbortSignal work with promises in Node.js?

`AbortSignal` provides a standard mechanism to cancel asynchronous operations. When you pass `{ signal: abortController.signal }` to Promise-based APIs like those in `timers/promises` or `fs/promises`, the Promise rejects with an `AbortError` when `abortController.abort()` is called. This prevents resource leaks and allows graceful shutdown of long-running operations without leaving hanging promises.

### How should I handle errors when using promisified functions?

Always use `try/catch` blocks with `await` or attach `.catch()` handlers to promise chains. Node.js normalizes errors through `handleErrorFromBinding` in [`lib/internal/util.js`](https://github.com/nodejs/node/blob/main/lib/internal/util.js), ensuring that rejected promises contain proper stack traces and libuv error codes (e.g., `ENOENT`). Unhandled rejections trigger `unhandledRejection` warnings and can crash your process in future Node.js versions, making explicit error handling mandatory for production code.

### When should I use Promise.allSettled instead of Promise.all?

Use `Promise.allSettled` when you need to execute multiple independent asynchronous operations and require the results of all attempts, regardless of individual failures. Unlike `Promise.all`, which immediately rejects when any single promise fails, `Promise.allSettled` returns an array of status objects indicating which operations succeeded and which failed. This is essential for batch processing tasks where partial success is acceptable and you need to report on all outcomes, such as processing multiple file reads or API calls where each result matters independently.