Debugging Turbopack Module Resolution Errors in Next.js

You switched dev to next dev --turbopack and a build that worked under Webpack now throws Module not found: Can't resolve '@/lib/utils' or Can't resolve 'some-pkg' — this guide diagnoses why Turbopack’s resolver disagrees with Webpack and how to fix each class of failure. Resolution is one node type in the demand-driven graph described in Turbopack Incremental Compilation; when its inputs are wrong, every downstream transform fails, so fixing resolution first is non-negotiable.

Turbopack ships in Next.js and is enabled with next dev --turbopack (stable as of Next.js 15). Its resolver is a fresh Rust implementation, not a port of Webpack’s enhanced-resolve, so configuration that Webpack tolerated implicitly must be made explicit.

Turbopack module resolution decision path An import specifier is classified as alias, relative, or bare, then resolved through tsconfig paths, extensions, and package exports conditions before failing or succeeding. Resolve an import specifier import '@/lib/x' classify specifier Alias / tsconfig path resolveAlias, paths Relative path resolveExtensions Bare package exports conditions Resolved feed transform node Module not found TURBOPACK=1 trace
Figure: Turbopack classifies each specifier, then resolves through aliases, extensions, or package exports; a miss surfaces as "Module not found", best traced with TURBOPACK=1.

How Turbopack Resolution Differs from Webpack

Three differences cause most regressions when teams flip the --turbopack flag:

  • No implicit extension guessing beyond the configured list. Webpack’s defaults plus loose project config often masked an import missing its .ts/.tsx. Turbopack resolves only the extensions in resolveExtensions (defaults: .tsx .ts .jsx .js .mjs .cjs .json), in order.
  • Stricter package exports/conditions handling. Turbopack honors the exports map and condition keys (import, require, node, default) precisely. A package that “worked” in Webpack because it fell back to main may now resolve to nothing if its exports map omits the subpath you import.
  • Aliases are not inherited from a Webpack config. Any resolve.alias, webpack() mutation, or tsconfig-paths-webpack-plugin you relied on is invisible to Turbopack. You restate aliases under turbopack.resolveAlias, and tsconfig paths are read directly.

Prerequisites & Reproducible Setup

# Next.js 15.3.x / Node 20+
npx create-next-app@latest tp-resolve-demo --ts --app --no-tailwind
cd tp-resolve-demo
npm pkg set scripts.dev="next dev --turbopack"
mkdir -p src/lib && echo "export const ping = () => 'pong';" > src/lib/utils.ts

Now import it from a page with import { ping } from '@/lib/utils'. If the @/* path is not configured for Turbopack, npm run dev throws Module not found: Can't resolve '@/lib/utils'.

Diagnosis Workflow

  1. Read the full error. Turbopack prints the failing specifier and the importing file. A @/-prefixed specifier points at an alias/paths problem; a bare name points at exports/install; a relative path points at extensions or a typo.
  2. Check tsconfig paths are mirrored. Turbopack reads compilerOptions.paths from tsconfig.json, but a custom baseUrl or a non-standard config location can break this; confirm the mapping resolves to a real file on disk.
  3. List what the package actually exports. For a bare-import failure run cat node_modules/<pkg>/package.json | grep -A20 '"exports"' and confirm the subpath you import is declared.
  4. Trace the resolver. Set TURBOPACK=1 for verbose internals and capture the attempted candidate paths:
# Verbose Turbopack internals (Next.js 15.3.x)
TURBOPACK=1 NEXT_TURBOPACK_TRACING=1 next dev --turbopack 2>&1 | tee resolve-trace.log
grep -i "resolve" resolve-trace.log
  1. Check for symlinks in a monorepo: ls -l node_modules/<pkg> — a symlink into a workspace package that is not declared as a dependency will not be tracked.

Solution Configuration

This next.config.js and matching tsconfig.json make every resolution input explicit. Both are complete and runnable.

// next.config.js — Next.js 15.3.x / Node 20+
/** @type {import('next').NextConfig} */
const nextConfig = {
  turbopack: {
    // Restate aliases — Turbopack does NOT read a Webpack resolve.alias.
    // Keys are exact specifiers or end in /* to mirror tsconfig paths.
    resolveAlias: {
      '@/*': ['./src/*'],
      // Force a single React copy across symlinked monorepo packages.
      react: './node_modules/react',
      'react-dom': './node_modules/react-dom',
    },
    // Extensions are tried in this order. Add custom ones you import without an extension.
    resolveExtensions: ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs', '.json'],
    // Pick condition keys for packages with an exports map (browser-first here).
    resolveConditions: ['browser', 'import', 'require', 'default'],
  },
};

module.exports = nextConfig;
// tsconfig.json — read directly by Turbopack for paths
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },
    "moduleResolution": "bundler"
  }
}

When tsconfig.json paths and turbopack.resolveAlias both define @/*, keep them identical. Drift between the two is a common cause of an editor that resolves the import while the dev server does not.

Verification

After saving the config, restart the dev server — Turbopack does not hot-reload next.config.js, so a stale process keeps throwing the old error. A clean start prints the route compiling without a Module not found line, and the page renders the imported value:

rm -rf .next && next dev --turbopack
# expect: "✓ Compiled / in <N>ms" and no "Module not found" in output

For a bare-package failure, confirm resolution lands on a real file by re-running the trace and grepping for the resolved absolute path:

TURBOPACK=1 next dev --turbopack 2>&1 | grep -i "<pkg-name>"
# expect a node_modules/<pkg>/dist/... path, not a "failed to resolve" line

Gotchas & Edge Cases

exports map omits the subpath. import x from 'pkg/internal' fails when pkg’s exports does not declare ./internal. You cannot force it from Turbopack config — either import a declared entry point or patch the package’s exports via your package manager’s override/patch mechanism.

Wrong condition resolved. A package that ships both ESM and CJS can resolve to the CJS branch if your resolveConditions order is wrong, surfacing as a runtime is not a function rather than a resolution error. Put import before require for ESM-first packages.

Monorepo symlink not tracked. A workspace package symlinked into node_modules but absent from the importing package’s dependencies/devDependencies will not be resolved or watched. Declare it as a workspace dependency. These same symlink gaps cause stale rebuilds, covered in Turbopack Incremental Compilation.

Case-sensitivity. Importing @/Lib/utils resolves on macOS and fails in Linux CI. Turbopack is case-sensitive on case-sensitive filesystems; match the on-disk casing exactly.