List Files in Directory JavaScript: Node.js + Browser Guide (2026)

Listing files in a directory with JavaScript looks simple, until you realize the language runs in two completely different worlds. In Node.js (server-side), you have the full fs module and can read any path the process owns. In the browser, you can’t touch the filesystem at all without explicit user permission, and even then, you use a completely different API (showDirectoryPicker()).

This 2026 refresh covers both. We’ll walk through every working approach for Node.js 22 LTS (sync, async, callback, recursive, filtering) and every browser-side option (File System Access API, the older webkitdirectory input fallback). Every code block is tested and runs as written. By the end you’ll know exactly which method to use for your case, capstone project, web uploader, build script, or backup tool.

Last updated: June 2026, by PIES Information Technology Solutions. Tested on Node.js 22.5 LTS, Chrome 126, Firefox 128, and Safari 17.

📌 Quick answer: In Node.js, use fs.readdirSync(path) for quick scripts or await fs.promises.readdir(path) for production async code (preferred in 2026). For recursive trees, pass { recursive: true } on Node 18+. In the browser, you cannot list arbitrary folders, use window.showDirectoryPicker() from the File System Access API to read a folder the user explicitly chooses, or fall back to <input type="file" webkitdirectory> for wider browser support.

Server-Side: Node.js fs Module (2026)

Node.js ships with the built-in fs (file system) module, no install needed. It gives you three flavors of every operation: synchronous, callback-based, and Promise-based. In 2026 the Promise-based API (fs.promises or import { readdir } from 'node:fs/promises') is the recommended default. Sync and callback APIs still work and are useful in specific cases.

All examples below assume Node.js 22 LTS (the current LTS as of June 2026) and modern ES2024 syntax. Use import if your package.json has "type": "module", or require otherwise.

Method 1: Synchronous: fs.readdirSync()

The simplest possible way. Blocks the event loop until the directory is read, then returns an array of filenames as strings. Use this only for one-off scripts, build tools, or CLI utilities, never inside a request handler.

import { readdirSync } from 'node:fs';

const files = readdirSync('./my-folder');
console.log(files);
// → [ 'index.js', 'package.json', 'README.md', 'src' ]

When to use: CLI scripts, build steps, migrations, anything that runs once and exits.

Downsides: Blocks every other operation in the Node process while reading. On a slow disk or a folder with 100K+ entries this freezes your app. Never use inside Express/Fastify handlers, never in serverless functions, never in a web server.

Method 2: Async with Promises: fs.promises.readdir() (PREFERRED 2026)

The modern default. Returns a Promise that resolves to the array of filenames. Pairs cleanly with async/await and never blocks the event loop. This is what you should reach for in any production code in 2026.

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

async function listFiles(dir) {
  try {
    const files = await readdir(dir);
    console.log(files);
    return files;
  } catch (err) {
    console.error(`Failed to read ${dir}:`, err.message);
    return [];
  }
}

await listFiles('./my-folder');

When to use: Any server, any API, any modern Node.js project. This is the default.

Bonus: Pass { withFileTypes: true } to get Dirent objects instead of strings, they include isFile() and isDirectory() methods without a second stat call:

const entries = await readdir('./my-folder', { withFileTypes: true });

for (const entry of entries) {
  if (entry.isDirectory()) {
    console.log(`📁 ${entry.name}`);
  } else {
    console.log(`📄 ${entry.name}`);
  }
}

Method 3: Callback-based: fs.readdir() (Legacy)

The original Node.js API. Still works in Node 22, still ships in core, and you’ll see it in every tutorial written before 2020. Avoid for new code, callback hell starts here.

import { readdir } from 'node:fs';

readdir('./my-folder', (err, files) => {
  if (err) {
    console.error('Read failed:', err.message);
    return;
  }
  console.log(files);
});

When to use: Maintaining legacy codebases that haven’t migrated to Promises yet. Don’t introduce this pattern in new code.

Why we don’t recommend it: Nested callbacks get unreadable fast. Error handling has to be repeated at every level. Composes poorly with the rest of modern async JavaScript.

Method 4: With File Details: fs.statSync() / fs.stat()

readdir only gives you filenames. To get size, modification date, or confirm whether each entry is a file or a folder, you need stat. The 2026 idiom combines readdir({ withFileTypes: true }) with stat only when you actually need size/date info:

import { readdir, stat } from 'node:fs/promises';
import { join } from 'node:path';

async function listFilesWithDetails(dir) {
  const entries = await readdir(dir, { withFileTypes: true });

  const detailed = await Promise.all(
    entries.map(async (entry) => {
      const fullPath = join(dir, entry.name);
      const stats = await stat(fullPath);
      return {
        name: entry.name,
        path: fullPath,
        isDirectory: entry.isDirectory(),
        sizeKB: (stats.size / 1024).toFixed(2),
        modified: stats.mtime.toISOString(),
      };
    })
  );

  return detailed;
}

const files = await listFilesWithDetails('./my-folder');
console.table(files);

Output looks like:

┌─────────┬────────────────┬────────────────────┬─────────────┬─────────┬──────────────────────────┐
│ (index) │ name           │ path               │ isDirectory │ sizeKB  │ modified                 │
├─────────┼────────────────┼────────────────────┼─────────────┼─────────┼──────────────────────────┤
│ 0       │ 'index.js'     │ 'my-folder/...'    │ false       │ '2.41'  │ '2026-06-12T08:14:22.000Z'│
│ 1       │ 'package.json' │ 'my-folder/...'    │ false       │ '0.83'  │ '2026-06-10T03:01:11.000Z'│
└─────────┴────────────────┴────────────────────┴─────────────┴─────────┴──────────────────────────┘

Using Promise.all() runs every stat call in parallel, much faster than awaiting them one at a time. For thousands of entries, consider p-limit to cap concurrency and avoid exhausting file handles.

Recursive Directory Listing

Need every file inside a folder and all its subfolders? Two ways to do it in 2026.

Option A, Native { recursive: true } (Node 18+, the easy way):

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

const all = await readdir('./project', { recursive: true });
console.log(all);
// → [ 'index.js', 'src', 'src/app.js', 'src/utils/helpers.js', ... ]

Returns paths relative to the starting directory, note src/app.js not ./project/src/app.js. Added in Node 18.17.0 and now stable. Use it whenever your Node version is >= 18.

Option B, Manual recursion (works on every Node version):

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

async function getAllFiles(dir) {
  const entries = await readdir(dir, { withFileTypes: true });

  const files = await Promise.all(
    entries.map(async (entry) => {
      const fullPath = join(dir, entry.name);
      if (entry.isDirectory()) {
        return getAllFiles(fullPath); // recurse
      }
      return fullPath;
    })
  );

  return files.flat();
}

const all = await getAllFiles('./project');
console.log(all);
// → [ '/abs/path/project/index.js', '/abs/path/project/src/app.js', ... ]

When to use which: Prefer the native { recursive: true } option for simplicity. Use the manual version when you need fine-grained control (skipping node_modules, stopping at a depth limit, returning absolute paths, or running custom logic per directory).

Stack overflow warning: Recursion on extremely deep or wide trees (think: an unbounded user upload directory) can blow the call stack. For those cases, switch to an iterative queue-based walk, push directories into an array and process them in a while loop instead of recursing.

Filtering by Extension

Most real-world directory listings need filtering, only .js files, only images, only PDFs. JavaScript gives you two clean options: path.extname() or a regex.

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

// Method 1: path.extname: cleanest for a single extension
const files = await readdir('./uploads');
const jsFiles = files.filter((f) => extname(f) === '.js');
const images = files.filter((f) =>
  ['.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(extname(f).toLowerCase())
);

// Method 2: regex: flexible for patterns
const pdfsAndDocs = files.filter((f) => /\.(pdf|docx?)$/i.test(f));

console.log({ jsFiles, images, pdfsAndDocs });

Tip: extname() returns the dot prefix (.js, not js) and is case-sensitive, normalize with .toLowerCase() before comparing if you accept user-uploaded files on Windows or macOS, where extension casing is unreliable.

Browser-Side: File System Access API (2026)

In the browser, JavaScript cannot list arbitrary folders on the user’s machine, that would be a massive security hole. What you can do is ask the user to pick a folder, then list its contents. That’s the File System Access API, anchored by window.showDirectoryPicker().

This API is fully supported in Chrome, Edge, and Opera as of 2026. Firefox and Safari still don’t ship it, for those browsers, fall back to webkitdirectory (next section).

async function listSelectedFolder() {
  try {
    // Triggers the OS folder picker dialog
    const dirHandle = await window.showDirectoryPicker();

    const entries = [];
    for await (const [name, handle] of dirHandle.entries()) {
      entries.push({
        name,
        kind: handle.kind, // 'file' or 'directory'
      });
    }

    console.log(entries);
    return entries;
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('User canceled folder pick.');
    } else {
      console.error('Folder pick failed:', err);
    }
  }
}

document.querySelector('#pick-folder').addEventListener('click', listSelectedFolder);

Security model: The browser shows a permission prompt each time. The user explicitly grants access, there’s no way to silently scan their disk. Permissions don’t persist by default; you can request persistent access via navigator.permissions, but the user has to approve it.

When to use: Web file managers, in-browser code editors (CodeSandbox-style), photo organizers, build-once-run-anywhere desktop replacements. Anywhere the user benefits from picking a folder and the app reading its contents directly without a server round trip.

Recursive walk: Iterate the same way, call dirHandle.entries() on each nested directory. The API is async-iterable from the ground up.

Old Browser Approach: input[type=file] webkitdirectory

For maximum browser support, including Firefox, Safari, and older mobile browsers, the legacy webkitdirectory attribute on a file input still works in 2026 despite its name. The user picks a folder, and the input returns every file inside (one level deep or recursive depending on the browser).

<input type="file" id="folder-input" webkitdirectory directory multiple />

<script>
document.querySelector('#folder-input').addEventListener('change', (event) => {
  const files = Array.from(event.target.files);

  const summary = files.map((file) => ({
    name: file.name,
    path: file.webkitRelativePath, // 'my-folder/subdir/file.js'
    sizeKB: (file.size / 1024).toFixed(2),
    type: file.type,
  }));

  console.table(summary);
});
</script>

When to use: When you need to support Firefox or Safari users, or when you just want a list of files (no writing back to disk). Works on every browser shipped since 2016.

Limits: Read-only, the browser hands you File objects you can upload or process, but you can’t write back. For two-way filesystem access, the File System Access API is the only option (and only in Chromium browsers).

Real-World Examples

Build a file uploader that previews directory contents: Combine webkitdirectory with a preview pane. List names, sizes, and types before the user confirms upload, great UX for capstone projects involving bulk file submission.

Generate a file index page from a directory: Use readdir({ recursive: true }) in a Node build script. Walk your docs/ folder, generate a JSON manifest, and ship it as the index for a static site. Same pattern Astro and Eleventy use internally.

Image gallery from a folder: In Node, filter for image extensions and serve them through Express. In the browser, use showDirectoryPicker + handle.getFile() to load images into an offline gallery, no upload needed.

Backup script that lists changed files: Combine readdir({ recursive: true, withFileTypes: true }) with stat() and compare mtime against a stored timestamp. Sync only changed files. Pairs well with our Node.js project collection if you want a working starter template.

Common Errors and Fixes

ENOENT: no such file or directory: The path you passed doesn’t exist (or has a typo). Print the path before calling readdir: console.log(resolve(dir)). Most often this is a relative-path bug, the process’s working directory isn’t where you think it is. Always use path.resolve() or __dirname for absolute paths in scripts.

EACCES: permission denied: The process doesn’t have read permission on that directory. Either fix the file mode (chmod +r), run with the right user, or pick a directory you own. On macOS Sonoma+ and Windows 11, system folders need explicit OS-level permission grants regardless of chmod.

ENOTDIR: not a directory: You passed a file path to readdir. Check first with fs.statSync(path).isDirectory() before reading, or wrap with try/catch. Common when iterating a glob result that mixes files and folders.

Windows vs macOS/Linux path differences: Windows uses \, Unix uses /. Never hardcode separators. Always use path.join('a', 'b', 'c') or path.sep. The native { recursive: true } option normalizes for you, but manual concatenation will silently break on the other OS.

Recursion stack overflow on huge trees: On directory trees with millions of entries or extreme depth, a naive recursive walk blows the call stack. Switch to a queue-based iterative walk:

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

async function walkIterative(start) {
  const queue = [start];
  const all = [];

  while (queue.length) {
    const dir = queue.shift();
    const entries = await readdir(dir, { withFileTypes: true });
    for (const entry of entries) {
      const full = join(dir, entry.name);
      if (entry.isDirectory()) queue.push(full);
      else all.push(full);
    }
  }

  return all;
}

Best Practices for 2026

  • Prefer Promises over callbacks. import { readdir } from 'node:fs/promises' is the default in 2026. Async/await reads like sync code without blocking the event loop.
  • Use { withFileTypes: true } instead of calling stat on every entry just to check isDirectory(), it cuts I/O by 50% on large directories.
  • Use the node: protocol for core module imports, import { readdir } from 'node:fs/promises'. It signals intent, avoids npm-package name collisions, and is the modern style.
  • Iterate with for await...of for very large directories, the browser File System Access API and Node’s experimental fs.opendir() both support async iteration, which streams entries instead of buffering all into memory.
  • Always handle errors. Wrap readdir in try/catch. Log the path that failed. Don’t let an ENOENT kill your whole script silently.
  • Normalize paths with path.join and path.resolve, never with string concatenation. Saves you a future cross-platform bug.
  • For the browser, feature-detect before using showDirectoryPicker: if ('showDirectoryPicker' in window) { ... } else { /* fallback to webkitdirectory */ }.

Frequently Asked Questions

How do I list all files in a directory using JavaScript?
In Node.js, use fs.readdirSync('./folder') for synchronous code or await fs.promises.readdir('./folder') for async (preferred in 2026). Both return an array of filenames. In the browser, you can’t list arbitrary folders for security reasons, use window.showDirectoryPicker() from the File System Access API to read a folder the user explicitly selects.
What is the difference between fs.readdir and fs.readdirSync?
fs.readdirSync blocks the event loop until the directory is read, fine for CLI scripts, dangerous in web servers. fs.readdir is the original callback-based async version, and fs.promises.readdir is the modern Promise-based one. In 2026, prefer fs.promises.readdir for everything except one-off scripts.
How do I list files recursively in Node.js?
On Node 18 or newer, pass { recursive: true } to readdir: await readdir('./project', { recursive: true }). For older versions or for fine-grained control (skipping node_modules, depth limits), write a small recursive function using readdir({ withFileTypes: true }) and recurse into entries where entry.isDirectory() is true.
Can JavaScript read a directory in the browser?
Not arbitrarily, for security, browsers don’t expose the filesystem to scripts. But the user can grant access. In Chromium browsers (Chrome, Edge, Opera), use the File System Access API: const dir = await window.showDirectoryPicker(), then iterate with for await (const [name, handle] of dir.entries()). For Firefox/Safari, fall back to <input type="file" webkitdirectory> which works in every modern browser.
How do I filter files by extension in Node.js?
Use path.extname(): files.filter(f => path.extname(f) === '.js'). For multiple extensions, check against an array: ['.png', '.jpg', '.webp'].includes(extname(f).toLowerCase()). Always lowercase for cross-platform safety, Windows treats extensions case-insensitively, Linux doesn’t. For complex patterns, a regex like /\.(pdf|docx?)$/i is cleaner.
What does ENOENT no such file or directory mean?
It means the path you passed to readdir doesn’t exist. Most often this is a relative-path bug, your script’s working directory isn’t where you assume. Print the absolute path before reading: console.log(path.resolve(dir)). Use path.join(__dirname, 'sub') to anchor paths to your script’s location instead of the process’s working directory.
Is showDirectoryPicker supported in all browsers?
As of June 2026, showDirectoryPicker() works in Chrome, Edge, Opera, and other Chromium-based browsers. Firefox and Safari still don’t ship it. For those browsers, fall back to <input type="file" webkitdirectory multiple>, which is read-only but works everywhere. Always feature-detect: if ('showDirectoryPicker' in window) { ... }.
How do I get the file size and modified date when listing?
Combine readdir with fs.stat: read the entries, then call stat(fullPath) on each. Run them in parallel with Promise.all(). The stats object gives you stats.size (bytes), stats.mtime (last modified Date), stats.birthtime (created), plus stats.isFile() / stats.isDirectory(). For large directories, prefer readdir({ withFileTypes: true }) and only stat when you actually need size or date.
Which Node.js version supports readdir recursive option?
readdir({ recursive: true }) was added in Node.js 18.17.0 and became stable shortly after. By 2026 it’s safe to use on any active LTS (18, 20, 22). If you’re stuck on an older version, write your own recursive function using readdir({ withFileTypes: true }), it’s a 10-line helper.

📌 Ready to build something with Node.js?

Browse our Node.js projects with source code, validate user input with our JavaScript phone number regex guide, or pick a stack for your capstone in our best free web hosting for capstone projects 2026.

Final Recommendation

If you take only one rule from this guide: in 2026, default to import { readdir } from 'node:fs/promises' with { withFileTypes: true } and await the result. That single pattern handles 90% of Node.js file-listing tasks without blocking the event loop, without callback nesting, and without an extra stat call per entry.

For recursive walks, pass { recursive: true }, it’s been native since Node 18 and there’s no reason to maintain your own helper unless you need depth limits or directory filtering. For the browser, use showDirectoryPicker() when you control the user experience (Chromium-only), and fall back to <input type="file" webkitdirectory> when you need Firefox/Safari support.

🎯 Your next steps:

  1. Migrate any fs.readdirSync in your web server code to fs.promises.readdir, it’s a 1-line change with a big reliability win
  2. For new code, always pass { withFileTypes: true }, it cuts I/O by half on large directories
  3. Need a project to apply this on? Browse our Node.js projects with source code
  4. Building a capstone? See 150 best capstone project ideas for IT students 2026
  5. Need an editor for JavaScript? Our 2026 IDE comparison covers VS Code, Cursor, and Zed, all top picks for JS too
  6. Browse more JavaScript tutorials

Using a different approach to list files in JavaScript (glob libraries, fs-extra, fast-glob)? Drop it in the comments, we update this guide when a new pattern becomes the new standard.

Leave a Comment