jsguides

fs/promises module

The fs/promises module provides promise‑based versions of all the standard fs functions. Introduced in Node.js 10, it allows you to work with the file system using async/await syntax, avoiding callback nesting and making error handling more straightforward.

Syntax

const fs = require('fs').promises;           // CommonJS
import fs from 'fs/promises';                // ES modules
import { readFile, writeFile } from 'fs/promises'; // named imports

Parameters

Most fs/promises functions share the same parameters as their callback‑based counterparts, minus the callback.

ParameterTypeDefaultDescription
pathstring(required)File or directory path. Can be absolute or relative.
optionsobject or stringnullEncoding (e.g., 'utf8') or an object with encoding, flag, mode, etc.
datastring, Buffer, or Uint8Array(required for writes)Content to write to the file.

Examples

Reading a file with async/await

import { readFile } from 'fs/promises';

async function readConfig() {
  try {
    const data = await readFile('config.json', 'utf8');
    console.log('Config loaded:', JSON.parse(data));
  } catch (err) {
    console.error('Failed to read config:', err.message);
  }
}

readConfig();

The output shows the parsed JSON object — readFile returned the raw text, and JSON.parse turned it into a usable JavaScript object. The try/catch block catches any file-not-found or permission errors without crashing the process.

Config loaded: { port: 3000 }

The read example uses async/await, but fs/promises works equally well with .then() chains. The next example writes to a log file using the promise-chaining style, appending timestamps with the { flag: 'a' } option to avoid overwriting earlier entries.

Writing a file with promises

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

fs.writeFile('log.txt', `Accessed at ${new Date().toISOString()}\n`, { flag: 'a' })
  .then(() => console.log('Log appended'))
  .catch(err => console.error('Write failed:', err));

The promise resolves after the append completes successfully. If the file does not exist, flag: 'a' creates it automatically — unlike flag: 'w' which would truncate on first write.

Log appended

Beyond reading and writing individual files, fs/promises can create entire directory trees. The recursive: true option tells mkdir to create every missing parent directory along the path, which avoids the error you would get when an intermediate folder is absent.

Creating a directory recursively

import { mkdir } from 'fs/promises';

async function setupProject() {
  await mkdir('./project/src/utils', { recursive: true });
  console.log('Directory structure created');
}

setupProject();

Unlike mkdir without { recursive: true }, which throws ENOENT when a parent directory is missing, the recursive option quietly creates the full path. If the directories already exist, the call is a no-op — it won’t overwrite or error out.

Directory structure created

The examples so far handle one file at a time. When you need to read or write many files, doing them sequentially can add unnecessary latency. The next section shows how to run operations in parallel.

Common Patterns

Parallel file operations with Promise.all

import { readFile } from 'fs/promises';

async function readMultipleFiles(filePaths) {
  const promises = filePaths.map(path => readFile(path, 'utf8'));
  const contents = await Promise.all(promises);
  return contents; // array of file contents in same order
}

// Usage
readMultipleFiles(['a.txt', 'b.txt', 'c.txt'])
  .then(results => console.log('Files read:', results))
  .catch(err => console.error('One or more files failed:', err));

Promise.all resolves when every file has been read, or rejects immediately if any single read fails. This fail-fast behavior means you cannot tell which specific file caused the rejection just from the error — the .catch handler receives only the first rejection. If you need partial results, use Promise.allSettled instead, which waits for all operations and reports each one’s status individually.

Error handling in promise‑based fs

Because fs/promises functions return promises, you can use standard promise‑catching patterns:

import { unlink } from 'fs/promises';

async function safeDelete(path) {
  try {
    await unlink(path);
    console.log(`Deleted ${path}`);
  } catch (err) {
    if (err.code === 'ENOENT') {
      console.warn(`File ${path} did not exist, skipping`);
    } else {
      throw err; // re‑throw unexpected errors
    }
  }
}

Available functions

fs/promises exports promise-based versions of all the key file system operations: readFile, writeFile, appendFile, copyFile, rename, unlink (delete a file), mkdir, rmdir, rm, readdir, stat, lstat, access, open, chmod, chown, symlink, readlink, and more. Each returns a Promise that resolves with the same result that the callback-based version would pass to its callback.

When to use fs/promises vs fs

Use fs/promises for all new code. The callback-based fs module is still available but requires more boilerplate and is harder to use correctly with async/await. The fs.promises property and the fs/promises module export the same API — the dedicated import from 'fs/promises' form is preferred because it supports tree-shaking in bundlers.

For streaming large files, combine fs/promises with Node streams: use fs.createReadStream() and fs.createWriteStream() (these do not have promise equivalents, as streams are inherently event-driven) alongside await pipeline() from node:stream/promises.

Error handling with fs/promises

Every fs/promises function returns a rejected promise when the operation fails. The rejection value is a native Error with a code property — for example, ENOENT for a missing file, EACCES for a permission error, and EEXIST for conflicts with mkdir without { recursive: true }. Because rejections propagate through await like thrown exceptions, wrapping operations in try/catch or chaining .catch() is the standard pattern. Unlike the callback API, there is no need to check an err argument on every call.

When fs/promises Is the Better Fit

fs/promises is the better fit whenever your surrounding code already uses async and await. It keeps the file-system API in the same control-flow style as the rest of your application, which makes error handling and sequencing much easier to read.

It is also a good choice when you want to compose multiple operations. Reading, parsing, writing, and cleanup steps can be chained in a single try block without nesting callbacks. That makes the code easier to test and easier to scan for the order of operations.

See Also

  • fs module – Callback‑based file system API
  • stream – Stream‑based file processing