Fixing ERR_PACKAGE_IMPORT_NOT_DEFINED in Node.js Subpath Imports

intermediate💚 Node.js2026-06-16| Node.js v14.13.0+, v16.x, v18.x, v20.x+; Windows, macOS, Linux.

Error Message

Error [ERR_PACKAGE_IMPORT_NOT_DEFINED]: Package import specifier "#utils" is not defined in package "package.json" imported from /app/src/index.js
#esm#package-json#imports#subpath#nodejs

The Error Scenario

Nothing kills coding momentum like staring at a wall of spaghetti-like relative paths. I was refactoring a project to replace import { helper } from '../../../../utils/helper.js' with something cleaner. Node.js now natively supports Subpath Imports via the "imports" field in package.json, allowing you to use aliases that start with #.

Everything looked correct on paper. However, the moment I launched the app, it crashed with this specific error:

Error [ERR_PACKAGE_IMPORT_NOT_DEFINED]: Package import specifier "#utils" is not defined in package "package.json" imported from /app/src/index.js
    at new NodeError (node:internal/errors:399:5)
    at throwImportNotDefined (node:internal/modules/esm/resolve:408:9)

Why is this happening?

Node.js triggers this error when it sees an import starting with # but can't find a matching rule in your configuration. It recognizes the syntax as a subpath import, but the resolution logic hits a dead end.

Usually, the culprit is one of these four issues:

  • The "imports" field is missing or misplaced in package.json.
  • You have a typo in the key (e.g., defining "#util" but calling "#utils").
  • The path mapping doesn't start with the mandatory ./ prefix.
  • Your package.json has a syntax error, like a trailing comma, which causes Node.js to ignore the entire field.

The Solution: Correcting package.json

Fixing this requires a strictly formatted "imports" object. This field must live at the top level of your package.json, alongside "name" and "version".

1. Direct File Mapping

Use this if you want to map a specific alias to a single file:

{
  "name": "my-app",
  "type": "module",
  "imports": {
    "#utils": "./src/utils/index.js"
  }
}

2. Dynamic Pattern Mapping

In most projects, you'll want to map an entire folder. To do this, you must use the * wildcard on both sides of the mapping:

{
  "imports": {
    "#utils/*": "./src/utils/*.js"
  }
}

Now, import { log } from '#utils/logger' correctly points to ./src/utils/logger.js. Without that *, Node.js won't know how to resolve the sub-files.

Essential Rules to Remember

Node.js is notoriously rigid about how you structure these fields. One small slip-up results in a broken app.

The Hash Prefix is Mandatory

Unlike TypeScript paths or Webpack aliases, every key in the imports field must start with a #. This ensures they don't clash with scoped npm packages like @company/package.

Paths Must Be Relative

The target value must start with ./. It tells Node the path is relative to the package root. Using "src/lib.js" will fail; use "./src/lib.js" instead.

Watch for JSON Syntax Errors

If your package.json is invalid, Node.js won't just skip the bad line—it might fail to read the imports field entirely. I once spent 45 minutes debugging an import only to find a missing double quote in a completely different section of the file.

To stay safe, run your file through a validator. I use the JSON Formatter & Validator at ToolCraft to catch invisible syntax errors or stray commas that standard editors sometimes miss.

Advanced Usage: Conditional Imports

Subpath imports are powerful because they can swap files based on the environment. This is great for handling Node.js vs. Browser logic without a heavy bundler.

{
  "imports": {
    "#db-client": {
      "node": "./src/db/server-side.js",
      "default": "./src/db/client-side.js"
    }
  }
}

How to Verify the Fix

Once you've updated your configuration, don't just hope it works. Run these quick checks:

  • Check Your Version: Run node -v. You need at least Node.js 14.13.0 for full subpath import support.
  • Run a CLI Test: You can verify the resolution without starting your whole server:

node -e "import('#utils/logger').then(m => console.log('Success!'))"

  
  - **Restart Your Dev Server:** Tools like `nodemon` or `ts-node` sometimes cache `package.json`. If the error persists after a fix, a full terminal restart usually clears the ghost errors.

## Quick Checklist

  - Does every key in `"imports"` start with `#`?
  - Does every value start with `./`?
  - Is the `"imports"` field located at the root of `package.json`?
  - If mapping a directory, did you include the `*` wildcard on both sides?
  - Is your JSON syntax 100% valid?

Related Error Notes