How to Fix: `Cannot find module ‘unknown’` with absolute imports in `turbo`

5 min read

The Cannot find module ‘unknown’ error in a Turborepo setup usually appears when a dynamic import collides with absolute path aliases such as @/…, and the bundler or runtime cannot statically resolve what file should be loaded.

Understanding the Root Cause

This bug happens because dynamic imports and TypeScript path aliases solve different problems at different stages of the toolchain.

When you write something like this:

export async function loadMessages(locale: Locale) {
  return (await import(`@/messages/${locale}.json`)).default;
}

the @ alias is usually defined in tsconfig.json or jsconfig.json. That alias helps TypeScript and some bundlers understand that @ maps to a real folder such as src or app. But a dynamic import using a variable path must still be analyzed by the bundler at build time.

In a turbo monorepo, that resolution can fail for a few reasons:

  • The alias is configured in one package but not consistently shared across apps and packages.
  • The bundler cannot determine the full import graph from import(`@/…`).
  • The runtime receives a transformed module request that no longer points to a valid file.
  • Some frameworks support aliases for static imports, but not for every form of template-literal dynamic import.

The result is an opaque module resolution failure, often surfaced as Cannot find module ‘unknown’ instead of a clearer file path error.

In short: absolute aliases are fine for static imports, but they can break when used inside dynamic import expressions that the bundler cannot enumerate safely.

Step-by-Step Solution

The most reliable fix is to avoid using the alias inside the dynamic import path and switch to a pattern the bundler can resolve deterministically.

1. Replace alias-based dynamic imports with relative imports

If the importing file is near the target directory, use a relative path:

export async function loadMessages(locale: Locale) {
  return (await import(`../messages/${locale}.json`)).default;
}

This works better because the bundler sees a real filesystem-relative base path instead of an alias that may only exist in TypeScript config.

2. Prefer an explicit import map for known files

If the set of files is finite, define them explicitly. This is often the safest option in production apps.

const messageLoaders = {
  en: () => import('../messages/en.json'),
  fr: () => import('../messages/fr.json'),
  de: () => import('../messages/de.json')
} as const;

export async function loadMessages(locale: keyof typeof messageLoaders) {
  const loader = messageLoaders[locale];

  if (!loader) {
    throw new Error(`Unsupported locale: ${locale}`);
  }

  return (await loader()).default;
}

This approach gives the bundler a fully known import graph and avoids ambiguous runtime resolution.

3. Verify alias configuration across the monorepo

If you still need absolute imports elsewhere, make sure the alias is defined consistently.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

In a monorepo, check whether that config lives in:

  • the app-level tsconfig.json
  • a shared base config such as packages/typescript-config/base.json
  • framework-specific config that also needs to understand the alias

If the alias only exists in TypeScript but not in the bundler layer, static imports may appear to work inconsistently while dynamic imports fail completely.

4. Keep JSON/module resolution compatible

If you are dynamically importing JSON files, confirm your config supports it.

{
  "compilerOptions": {
    "resolveJsonModule": true,
    "esModuleInterop": true
  }
}

Also verify the files actually exist in the final app package and are not excluded by build rules.

5. Clear caches and rebuild turbo pipelines

Turborepo caching can preserve stale resolution state during debugging.

rm -rf .turbo
rm -rf node_modules
rm -rf apps/web/.next
pnpm install
pnpm turbo run build --force

If you use npm or yarn, replace the install command accordingly.

For most internationalization or file-loader cases, use an explicit loader map instead of alias-based template imports:

type Locale = 'en' | 'fr' | 'de';

const loaders: Record<Locale, () => Promise<{ default: Record<string, string> }>> = {
  en: () => import('../messages/en.json'),
  fr: () => import('../messages/fr.json'),
  de: () => import('../messages/de.json')
};

export async function loadMessages(locale: Locale) {
  return (await loaders[locale]()).default;
}

This removes the alias from the dynamic segment, improves type safety, and makes the build output predictable.

Common Edge Cases

  • Alias works in one app but not another: In a monorepo, each app may have different bundler settings even if they share TypeScript config.
  • Only production build fails: Development servers can be more forgiving, while production bundling requires statically analyzable imports.
  • JSON imports fail unexpectedly: Missing resolveJsonModule or framework-specific JSON handling can cause separate resolution errors.
  • Server-only vs client-side code: Some dynamic import patterns behave differently depending on whether code runs on the server, edge runtime, or browser bundle.
  • Generated files are missing: If message files are created during a build step, turbo task ordering may cause the import to run before files exist.
  • Overly dynamic template strings: Imports like import(`@/${section}/${name}`) are much harder for bundlers to analyze than a constrained explicit map.

FAQ

Can I still use @ absolute imports in Turborepo?

Yes. They are generally fine for static imports. The problem is specifically with dynamic imports where the final module path cannot be safely resolved by the bundler.

Why does the error say ‘unknown’ instead of the actual path?

Because the failure often happens after the import expression has been transformed internally. By that point, the bundler or runtime may no longer surface the original alias path cleanly, resulting in a generic module name.

What is the safest long-term fix?

Use explicit import maps for dynamic modules and reserve absolute aliases for static imports. This is the most stable approach across monorepo packages, build pipelines, and framework upgrades.

If you want to keep your monorepo reliable, treat alias-based dynamic imports as a portability risk. In turbo-powered apps, the most robust solution is usually the simplest one: make the import graph explicit.

Leave a Reply

Your email address will not be published. Required fields are marked *