Next.js "Module not found: Can't resolve 'fs'": Tested Fix
"Module not found: Can't resolve 'fs'" means a file that reads the filesystem ended up in code that runs in the browser. Browsers have no fs. The same error appears for child_process, net, tls, dns and worker_threads, the other Node-only modules, often from an npm package (a database driver, a Markdown loader) rather than your own import.
I reproduced it on Next.js 16.4.4 with Turbopack and with webpack, tried the common fixes, and clicked through the result in a browser.
The error
// lib/utils.ts
import fs from "fs";
export function slugify(s: string) { return s.toLowerCase().replace(/\s+/g, "-"); }
export function countFiles() { return fs.readdirSync(process.cwd()).length; }
// app/page.tsx
"use client";
import { slugify } from "../lib/utils"; // only the pure function
export default function Page() { return <p>{slugify("Hello World")}</p>; }Error: Turbopack build failed with 1 error:
./lib/utils.ts:1:1
Error: Module not found: Can't resolve 'fs'
Import traces:
Client Component Browser:
./lib/utils.ts [Client Component Browser]
./app/page.tsx [Client Component Browser]
./app/page.tsx [Server Component]Two things from this test. First, the client page only used slugify, which doesn't touch fs, and the build still failed: importing anything from a file pulls in the whole file, including its fs import. Second, read the import trace from the bottom up. It names the chain from the first "use client" file to the fs import, so you know exactly which import to move. webpack (next build --webpack) failed the same way with "Failed to compile. Module not found: Can't resolve 'fs'".
The fix: keep fs on the server
Split the file: browser-safe helpers in one module, filesystem code in another. Call the filesystem code in a server component and pass the result to the client component as a prop:
// lib/slug.ts: safe anywhere
export function slugify(s: string) { return s.toLowerCase().replace(/\s+/g, "-"); }
// lib/files.ts: server only
import "server-only";
import fs from "node:fs";
export function countFiles() { return fs.readdirSync(process.cwd()).length; }
// app/page.tsx: server component, no "use client"
import { countFiles } from "../lib/files";
import { Count } from "./Count";
export default function Page() { return <Count n={countFiles()} />; }
// app/Count.tsx
"use client";
import { slugify } from "../lib/slug";
export function Count({ n }: { n: number }) {
return <p>{slugify("Hello World")}: {n} files</p>;
}That built, the route stayed static (fs ran at build time), and the HTML contained the count. In the Pages Router the equivalent is reading files in getStaticProps or getServerSideProps, which never ship to the browser.
Add import "server-only" so it can't happen again
With import "server-only" at the top of the filesystem module, importing it from a client component fails with a clearer message:
Error: 'server-only' cannot be imported from a Client Component module
It should only be used from a Server Component.Turbopack printed that next to the original "Can't resolve 'fs'", plus a third message saying "you are using it in the Pages Router", which was wrong: this was the App Router. Go by the 'server-only' and import-trace lines.
Why fallback: { fs: false } is the wrong fix
The most common answer online tells webpack to replace fs with an empty module in the browser. The Turbopack version is a resolveAlias:
// next.config.ts: makes the error go away, not the problem
const config = {
webpack: (c, { isServer }) => {
if (!isServer) c.resolve.fallback = { ...c.resolve.fallback, fs: false };
return c;
},
turbopack: { resolveAlias: { fs: { browser: "./lib/empty.ts" } } },
};Both builds passed. Then I clicked a button that called the fs function in the browser. Both the webpack and the Turbopack build threw "readdirSync is not a function". The build error became a runtime error that only shows up when a user clicks. The fallback is only reasonable for a package that imports fs but never calls it in the browser, and even then splitting the import is cleaner.
When it's an npm package
- Check the trace: if it ends in node_modules, a client component is importing a server library (database clients, gray-matter, nodemailer and similar).
- Move that import into a server component, a server action, or a route handler, and send the data to the client.
- If the package has a browser build, import that entry instead; its docs will say.
A related warning
Once fs is on the server, Turbopack may warn "Dynamic filesystem access causes tracing of the whole project" for something like fs.readdirSync("."). It means Next.js can't tell which files you need, so it includes everything in the server output. Read from a fixed folder instead, such as path.join(process.cwd(), "content").
Related
- params should be awaited: tested on Next.js 16
- Next.js hydration error: 6 causes tested
- Next.js development