Module not found: Can't resolve 'fs'

⚡ Fastest fix

fs is a Node server module that doesn't exist in the browser. Server-only code leaked into a client bundle. Move the fs code to a server-only place — in Next.js that's a Server Component, Route Handler, or getServerSideProps/getStaticProps:

// ✅ Next.js — fs runs on the server only
export async function getServerSideProps() {
  const fs = await import('node:fs/promises');
  const data = await fs.readFile('data.json', 'utf8');
  return { props: { data } };
}

What you're seeing

Module not found: Can't resolve 'fs'

Import trace for requested module:
./lib/read-config.js
./components/Sidebar.jsx     <-- a client component pulled in server-only code
./app/page.jsx

Follow the "Import trace". It shows the chain from a browser file down to the module that imports fs. The fix goes at the point where client code first imports something server-only.

30-second triage

Fix 1 — Keep fs in server-only code

When: your own component or util reads files and also renders in the browser.

// ❌ a Client Component importing fs
'use client';
import fs from 'node:fs';         // bundled for the browser -> Can't resolve 'fs'

// ✅ read on the server, pass data down as props/args
// app/page.jsx (Server Component — no 'use client')
import fs from 'node:fs/promises';
export default async function Page() {
  const cfg = JSON.parse(await fs.readFile('config.json', 'utf8'));
  return <Sidebar config={cfg} />;   // Sidebar is the client part, no fs
}

In the App Router, files are Server Components by default — fs is fine there. The moment a file has 'use client', it (and everything it imports) is bundled for the browser, where fs doesn't exist.

Fix 2 — Split a shared module

When: one util file exports both a file-reading helper and something the client needs.

// lib/config.server.js   — server only, may use fs
import fs from 'node:fs/promises';
export const loadConfig = () => fs.readFile('config.json', 'utf8');

// lib/config.shared.js   — safe for the browser, no Node built-ins
export const DEFAULTS = { theme: 'light' };

Bundlers pull in the whole file you import from. If the client only needs DEFAULTS, keep it in a file that never touches fs so the server-only half is never dragged into the client bundle. (A .server.js/server-only convention makes the boundary obvious.)

Fix 3 — Dynamically import a Node-only dependency

When: a package uses fs internally and you only call it on the server.

// only loaded when this server path runs — excluded from the client bundle
export async function POST(req) {
  const { parseFile } = await import('some-node-only-lib');
  return Response.json(await parseFile('/tmp/x'));
}

A top-level import is always bundled; a dynamic await import() inside a server-only function keeps the Node dependency out of the browser build entirely.

Fix 4 — Stub fs in the client build (last resort)

When: a dependency references fs but the code path never runs in the browser, and you can't change it.

// next.config.js
module.exports = {
  webpack: (config, { isServer }) => {
    if (!isServer) config.resolve.fallback = { ...config.resolve.fallback, fs: false };
    return config;
  },
};

fs: false replaces fs with an empty module in the client build. This only silences the error — if that code actually executes in the browser it will still break at runtime. Use it only when the fs path is truly dead on the client.

Fix 5 — Vite specifics

When: the same error under Vite (which only targets the browser).

// keep fs in an SSR entry / API route / node script, never client code.
// for an SSR-only dependency, mark it external so it isn't bundled:
// vite.config.js
export default {
  ssr: { external: ['some-node-only-lib'] },
};

Why this happens

fs, along with path, crypto, net, and child_process, is a Node.js built-in — it only exists in a Node runtime. A bundler building for the browser has to resolve every import to something it can ship, and there is no browser fs to resolve to, so it stops with Can't resolve 'fs'. The import itself isn't the bug; the bug is that a module which uses a server-only API got pulled into a client bundle. That happens when server code is imported (directly or transitively) by a Client Component, a browser entry, or a shared file. Fixing it always means re-drawing the server/client boundary so fs stays on the server — not installing a package.

By tool

ToolKeep fs inEscape hatch
Next.js (App Router)Server Components, Route Handlersresolve.fallback.fs = false
Next.js (Pages)getServerSideProps/getStaticProps, API routessame webpack fallback
webpack (plain)server entry onlyresolve.fallback.fs = false
ViteSSR entry / node scriptsssr.external

✓ Confirm it's fixed

  • The production build (next build / vite build) completes with no "Can't resolve 'fs'".
  • Search your client bundle for node:fs/require("fs") — it shouldn't be there.
  • The file-reading feature still works when the page runs (data arrives via props/an API response, not client fs).

Frequently Asked Questions

What does 'Module not found: Can't resolve fs' mean?

fs is Node's file-system module and it does not exist in the browser. The error means your bundler (webpack, Next.js, Vite) is trying to include code that imports fs in a bundle destined for the browser. The bundler can't find a browser version of fs, so it fails. The real problem is that server-only code ended up in a client bundle.

How do I fix 'Can't resolve fs' in Next.js?

Only use fs in server-only code: inside getServerSideProps/getStaticProps, a Route Handler, an API route, or a Server Component. Never import a module that uses fs into a Client Component ('use client'). If a shared file both reads files and runs in the browser, split it so the fs part stays server-side.

Should I add a webpack fallback for fs?

Setting resolve.fallback.fs = false tells the bundler to replace fs with an empty stub in the client build. This only works if the fs code path never actually runs in the browser — it silences the build error but the code must be dead on the client. Prefer moving the code server-side; use the stub only for a dependency you can't change.

Why does a library I installed trigger this?

Some npm packages are Node-only (they read files, use path, crypto, or child_process) but got imported into client code. Either import that library only in server code, find a browser-compatible alternative, or dynamically import it so it's excluded from the client bundle. Check the package docs for a browser/ESM build.

How do I fix it in Vite?

Vite targets the browser, so importing fs in client code fails the same way. Keep fs in server code (a Vite SSR entry, an API route, or a Node script), not in anything shipped to the browser. If a dependency insists on Node built-ins, either replace it or, for SSR-only usage, mark it external so it isn't bundled for the client.

Can I use fs in the browser at all?

No — browsers have no file-system access like Node's fs. To read a user's file in the browser, use an <input type="file"> with the File API / FileReader; for persistent storage use IndexedDB or the File System Access API. Anything that truly needs fs must run on the server.

More build & module errors

Browse the full reference for Node.js, bundler, and module errors — exact message, cause, and fix.

All Error References Cannot find module import statement outside a module
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.