List files in a directory with Node.js

Read entry names with readdir, filter Dirent objects by type, or walk incrementally when directory size and symlink behavior matter.

Best practices
Illustration of files, modules, and dependencies

Use fs.readdir() or its promise and synchronous variants to list directory entries. By default, the result includes names of directories and other entries as well as files, and it does not recurse.

Choose an API style

A CommonJS callback example:

const fs = require('node:fs');
fs.readdir('/path/to/dir', (error, names) => {
  if (error) {
    console.error('Directory read failed:', error.message);
    return;
  }
  names.forEach((name) => console.log(name));
});

In an ES module:

import { readdir } from 'node:fs/promises';
const names = await readdir('/path/to/dir');
console.log(names);

Or, in a small blocking CommonJS script:

const fs = require('node:fs');
const names = fs.readdirSync('/path/to/dir');
console.log(names);

Replace the example directory with an existing path. Relative paths are resolved from the current working directory.

Filter by entry type

With the promise-based readdir imported above:

const entries = await readdir('/path/to/dir', { withFileTypes: true });
for (const entry of entries) {
  if (entry.isFile()) console.log('File:', entry.name);
  if (entry.isDirectory()) console.log('Directory:', entry.name);
}
const scripts = entries
  .filter((entry) => entry.isFile() && entry.name.endsWith('.js'))
  .map((entry) => entry.name);

A filename suffix is not proof of type. Dirent type checks describe the directory entry itself, so a symlink is not classified as its target file. Use a deliberate stat() policy if you want to follow links, and handle disappearing or inaccessible targets.

Walk subdirectories

This ES module generator yields regular files and does not follow symlink entries:

import { opendir } from 'node:fs/promises';
import path from 'node:path';

async function* walk(directory) {
  const handle = await opendir(directory);
  for await (const entry of handle) {
    const fullPath = path.join(directory, entry.name);
    if (entry.isDirectory()) yield* walk(fullPath);
    else if (entry.isFile()) yield fullPath;
  }
}

for await (const file of walk('/path/to/dir')) console.log(file);

The asynchronous directory iterator closes its handle when iteration finishes or exits. Recursion retains handles for ancestor directories, so very deep trees can still require a more bounded traversal strategy. Errors propagate; decide whether your application should stop or report and skip inaccessible branches.

Modern readdir() also supports { recursive: true }, added in Node 18.17.0 and 20.1.0. You do not always need your own recursion. Choose explicit traversal when type filtering, link policy, or incremental output requires more control. A glob implementation is useful for richer matching rules.

FAQ

Is the output sorted? Do not depend on filesystem enumeration order. Sort names or results explicitly when stable order is required.

Why do I get ENOENT? Check the actual base path and whether the entry disappeared. To locate resources relative to a module, use a module-derived path instead of an accidental working-directory dependency.

What about hundreds of thousands of entries? readdir() materializes its result. opendir() supports incremental iteration and has existed since Node 12.12.0, not only Node 20. Avoid collecting all yielded results if the goal is bounded memory.

References

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting