Skip to content
Latchkey

Node "Cannot use import statement outside a module" in CI - Fix It

This SyntaxError means Node parsed a file as CommonJS but the file uses ES import syntax. The runtime never reaches your logic; parsing fails first.

What this error means

A node command throws SyntaxError: Cannot use import statement outside a module at the first import line. The file uses import/export but Node is loading it as CommonJS.

node
import express from 'express';
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at internalCompileFunction (node:internal/vm:73:18)

Diagnose it: the shell in CI is not your shell

Package scripts run under a different shell, a different PATH, and a non-interactive environment on a runner. Most scripts that fail only in CI are relying on something the login shell gave them locally: a tool on PATH, an environment variable from a dotfile, or a TTY.

Terminal
# what the script can actually see
npm run env | grep -E "^(PATH|NODE_ENV|CI)=" 

# is the binary on PATH for the script, not just for you?
npm exec -- which <tool> || echo "not resolvable from npm scripts"

# run the exact script with tracing
sh -x -c "$(node -p "require('./package.json').scripts.build")"

Common causes

No "type": "module" in package.json

A .js file with import syntax is parsed as CommonJS unless the nearest package.json declares type module.

Running uncompiled TypeScript or JSX directly

Node runs raw source that should have passed through a build or loader (tsc, tsx, babel) but did not.

How to fix it

Declare the package as ESM

  1. Add "type": "module" to package.json so .js files are treated as ES modules.
  2. Make sure your require() calls are converted to import or moved to .cjs files.
package.json
{
  "type": "module"
}

Transpile before running

  1. Run the build (tsc, esbuild, babel) so the emitted CommonJS no longer uses import syntax.
  2. Execute the compiled output instead of the source.
GitHub Actions
- run: npm run build
- run: node dist/server.js

Make failures fail the job

A multi-command script can report success while a middle command failed, which produces the worst kind of CI result: a green build that shipped something broken.

.github/workflows/ci.yml
# pipefail is NOT set by default in every runner shell
- name: Build
  shell: bash
  run: |
    set -euo pipefail
    npm run build | tee build.log

How to prevent it

  • Pick one module system per package, set the matching "type" field, and run compiled output in CI rather than raw source that depends on a loader being present.

Frequently asked questions

What causes Node "Cannot use import statement outside a module" in CI?
There are 2 common causes: no "type": "module" in package.json and running uncompiled typescript or jsx directly. A .js file with import syntax is parsed as CommonJS unless the nearest package.json declares type module.
How do I fix Node "Cannot use import statement outside a module" in CI?
There are 2 fixes depending on which cause you have: declare the package as esm and transpile before running. Work through them in order, since the first is the most common.
What does Node "Cannot use import statement outside a module" in CI actually mean?
A node command throws SyntaxError: Cannot use import statement outside a module at the first import line.
How do I stop Node "Cannot use import statement outside a module" in CI happening again?
Pick one module system per package, set the matching "type" field, and run compiled output in CI rather than raw source that depends on a loader being present.

Related guides

References

Not every red build is your code. Latchkey repairs the ones that are not, on the runner. Start free → 30-day trial · No credit card