The errors you are actually going to hit, in Node's own words.
40 entries across 25 error names and codes.
Every error on this page is real. Each snippet declares the error it expects
-- by class, like TypeError, or by code, like ENOENT -- and the build
runs it and checks: if it does not die, or dies with something else, the build fails and this
page is not written. So what you see under each snippet is what Node printed for exactly that
code. The directory each snippet ran in is shown as /app, so the paths are the same
on any machine; the names, codes, messages and line numbers are untouched.
It will not even start
Raised while Node is still reading your file, before a single line of it has run -- which is why a console.log you added to debug it prints nothing. Look at what is missing from these: there are no "at ..." stack frames pointing into your code, because none of it ever executed.
What node printed as it died, when the build ran it
$ node example.mjs
file:///app/example.mjs:7
SyntaxError: Unexpected end of input
at compileSourceTextModule (node:internal/modules/esm/utils:355:16)
at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:90:18)
at #translate (node:internal/modules/esm/loader:441:20)
at afterLoad (node:internal/modules/esm/loader:509:29)
at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:514:12)
at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:567:36)
at afterResolve (node:internal/modules/esm/loader:614:52)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:620:12)
at node:internal/modules/esm/loader:639:32
at TracingChannel.tracePromise (node:diagnostics_channel:538:22)
Node.js v26.7.0
What it means. Node reached the end of the file still inside an open {. The location it gives is the end of the file, which is where it noticed -- not where the missing brace belongs. Count your braces upwards from there, or let an editor match them for you.
The fix. Close the if with its own } before the function’s.
What node printed as it died, when the build ran it
$ node example.mjs
file:///app/example.mjs:2
const res = await fetch("http://127.0.0.1:1/");
^^^^^
SyntaxError: Unexpected reserved word
at compileSourceTextModule (node:internal/modules/esm/utils:355:16)
at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:90:18)
at #translate (node:internal/modules/esm/loader:441:20)
at afterLoad (node:internal/modules/esm/loader:509:29)
at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:514:12)
at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:567:36)
at afterResolve (node:internal/modules/esm/loader:614:52)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:620:12)
at node:internal/modules/esm/loader:639:32
at TracingChannel.tracePromise (node:diagnostics_channel:538:22)
Node.js v26.7.0
What it means.await is allowed at the top level of a module, and inside an async function, and nowhere else. The message is unhelpful -- "Unexpected reserved word" -- but the carets sit exactly under await, and that pairing is worth recognising on sight, because it is always this.
The fix. Write async function load(). Every caller then gets a promise back and has to await it in turn. Lesson 27.
SyntaxErrorimport in a CommonJS file
import in a CommonJS file
dies with SyntaxError
example.cjs
1
2
3import { readFileSync } from "node:fs";
console.log(typeof readFileSync);
What node printed as it died, when the build ran it
$ node example.cjs
(node:<pid>) Warning: Failed to load the ES module: /app/example.cjs. Make sure to set "type": "module" in the nearest package.json file or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
/app/example.cjs:1
import { readFileSync } from "node:fs";
^^^^^^
SyntaxError: Cannot use import statement outside a module
at wrapSafe (node:internal/modules/cjs/loader:1866:18)
at Module._compile (node:internal/modules/cjs/loader:1908:20)
at Object..js (node:internal/modules/cjs/loader:2074:10)
at Module.load (node:internal/modules/cjs/loader:1656:32)
at Module._load (node:internal/modules/cjs/loader:1448:12)
at wrapModuleLoad (node:internal/modules/cjs/loader:261:19)
at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5)
at node:internal/main/run_main_module:33:47
Node.js v26.7.0
What it means. A .cjs file is CommonJS by definition, and CommonJS has require, not import. The warning above the error tells you both ways out. The same error appears in a .js file whose nearest package.json says "type": "commonjs" -- which npm init now writes by default.
The fix. Rename the file to .mjs, or set "type": "module" in package.json, or use require. Lesson 21.
What node printed as it died, when the build ran it
$ node example.mjs
file:///app/example.mjs:2
const fs = require("node:fs");
^
ReferenceError: require is not defined in ES module scope, you can use import instead
at file:///app/example.mjs:2:12
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
What it means. The reverse of the entry above. require does not exist in an ES module. This file is a module because its name ends in .mjs; a .js file becomes one too the moment Node sees an import in it, which is how a file that used to work breaks when someone adds one line to the top.
The fix. Use import fs from "node:fs". If you genuinely need require inside a module, import { createRequire } from "node:module" gives you one. Lesson 21.
SyntaxErrorDeclaring the same name twice
Declaring the same name twice
dies with SyntaxError
example.mjs
1
2
3let total = 0;
const items = [1, 2, 3];
let total = items.length;
What node printed as it died, when the build ran it
$ node example.mjs
file:///app/example.mjs:3
let total = items.length;
^
SyntaxError: Identifier 'total' has already been declared
at compileSourceTextModule (node:internal/modules/esm/utils:355:16)
at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:90:18)
at #translate (node:internal/modules/esm/loader:441:20)
at afterLoad (node:internal/modules/esm/loader:509:29)
at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:514:12)
at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:567:36)
at afterResolve (node:internal/modules/esm/loader:614:52)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:620:12)
at node:internal/modules/esm/loader:639:32
at TracingChannel.tracePromise (node:diagnostics_channel:538:22)
Node.js v26.7.0
What it means.let and const refuse to declare a name that already exists in the same scope. var allowed it silently, which is one of the reasons it is no longer used. This is caught before anything runs.
The fix. Assign instead of redeclaring -- total = items.length -- or pick a different name.
ERR_UNSUPPORTED_TYPESCRIPT_SYNTAXA TypeScript enum, run directly
A TypeScript enum, run directly
dies with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX
example.ts
1
2
3enum Colour { Red, Green }
console.log(Colour.Green);
What node printed as it died, when the build ran it
$ node example.ts
file:///app/example.ts:1
enum Colour { Red, Green }
^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
at parseTypeScript (node:internal/modules/typescript:57:40)
at processTypeScriptCode (node:internal/modules/typescript:129:37)
at stripTypeScriptModuleTypes (node:internal/modules/typescript:172:22)
at Module._compile (node:internal/modules/cjs/loader:1888:15)
at Object..js (node:internal/modules/cjs/loader:2074:10)
at Module.load (node:internal/modules/cjs/loader:1656:32)
at Module._load (node:internal/modules/cjs/loader:1448:12)
at wrapModuleLoad (node:internal/modules/cjs/loader:261:19)
at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5)
at node:internal/main/run_main_module:33:47 {
code: 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX'
}
Node.js v26.7.0
What it means. Node runs .ts files by erasing the types -- replacing annotations with spaces -- and running what is left. That only works for syntax that is pure type information. An enum generates real JavaScript at runtime, so there is nothing to erase it to, and Node refuses rather than guess.
The fix. Use a plain object with as const, or a union of string literals. Or run the file through the TypeScript compiler. Lesson 47.
Names and values
The code ran, and asked for something that is not there, or is not what it was assumed to be.
What node printed as it died, when the build ran it
$ node example.mjs
file:///app/example.mjs:2
console.log("hello", username);
^
ReferenceError: username is not defined
at file:///app/example.mjs:2:22
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
What it means. JavaScript names are case-sensitive, so username and userName are different names, and only one of them exists. The caret points at the one Node could not find.
The fix. Fix the spelling. An editor with the TypeScript language service underlines this before you run anything, even in plain JavaScript.
ReferenceErrorUsing a variable before its line
Using a variable before its line
dies with ReferenceError
example.mjs
1
2console.log(limit);
const limit = 10;
What node printed as it died, when the build ran it
$ node example.mjs
file:///app/example.mjs:1
console.log(limit);
^
ReferenceError: Cannot access 'limit' before initialization
at file:///app/example.mjs:1:13
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
What it means. The name does exist -- const and let are known to the whole block from its first line -- but it has no value until its declaration runs. The region above the declaration is called the temporal dead zone, and reading a name inside it is an error rather than undefined. Lesson 10.
The fix. Move the declaration above its first use.
What node printed as it died, when the build ran it
$ node example.mjs
file:///app/example.mjs:3
console.log(user.name);
^
TypeError: Cannot read properties of undefined (reading 'name')
at file:///app/example.mjs:3:18
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
What it means. The single most common error in JavaScript. find returns undefined when nothing matches, and asking undefined for a property throws. The message names the property it was reading, which tells you which value was missing.
The fix. Decide what should happen when there is no match: if (!user) ..., or user?.name to get undefined instead of an error. Lesson 7.
TypeErrorCalling something that is not a function
Calling something that is not a function
dies with TypeError
example.mjs
1
2const config = { port: 3000 };
config.port();
What node printed as it died, when the build ran it
$ node example.mjs
file:///app/example.mjs:2
config.port();
^
TypeError: config.port is not a function
at file:///app/example.mjs:2:8
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
What it means. Parentheses call a function, and config.port is a number. The usual real cause is a misspelled method name, which reads as undefined, and "undefined is not a function" -- or, as here, a property that holds data rather than behaviour.
The fix. Check what the value actually is: console.log(typeof config.port).
TypeErrorReassigning a const
Reassigning a const
dies with TypeError
example.mjs
1
2const retries = 3;
retries = retries - 1;
What node printed as it died, when the build ran it
$ node example.mjs
file:///app/example.mjs:2
retries = retries - 1;
^
TypeError: Assignment to constant variable.
at file:///app/example.mjs:2:9
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
What it means.const means the name cannot be pointed at a different value. It does not mean the value cannot change: a const array can still be pushed to. Lesson 4 runs both.
The fix. Use let for a name you intend to reassign.
What node printed as it died, when the build ran it
$ node example.mjs
file:///app/example.mjs:2
settings.theme = "light";
^
TypeError: Cannot assign to read only property 'theme' of object '#<Object>'
at file:///app/example.mjs:2:16
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
What it means.Object.freeze makes an object read-only. In old, sloppy-mode scripts the write is silently ignored; an ES module is always strict, so it throws instead, which is the behaviour you want -- a write that silently does nothing is a bug you find much later.
The fix. Make a changed copy rather than editing it: { ...settings, theme: "light" }. Lesson 14.
What node printed as it died, when the build ran it
$ node example.mjs
file:///app/example.mjs:2
return countdown(n - 1);
^
RangeError: Maximum call stack size exceeded
at countdown (file:///app/example.mjs:2:3)
at countdown (file:///app/example.mjs:2:10)
at countdown (file:///app/example.mjs:2:10)
at countdown (file:///app/example.mjs:2:10)
at countdown (file:///app/example.mjs:2:10)
at countdown (file:///app/example.mjs:2:10)
at countdown (file:///app/example.mjs:2:10)
at countdown (file:///app/example.mjs:2:10)
at countdown (file:///app/example.mjs:2:10)
at countdown (file:///app/example.mjs:2:10)
Node.js v26.7.0
What it means. Every call takes a slot on the call stack, and this function calls itself forever, so the stack runs out. Node shows only the top ten frames, all identical -- which is itself the clue.
The fix. Add the base case: if (n <= 0) return 0;. If the recursion is genuinely deep, rewrite it as a loop.
RangeErrorAn impossible array length
An impossible array length
dies with RangeError
example.mjs
1
2const size = -1;
const slots = new Array(size);
What node printed as it died, when the build ran it
$ node example.mjs
file:///app/example.mjs:2
const slots = new Array(size);
^
RangeError: Invalid array length
at file:///app/example.mjs:2:15
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
What it means. An array length must be a whole number from zero to about four billion. A negative or fractional size usually means an arithmetic slip upstream -- here, a count that went below zero.
The fix. Find where the size is computed and guard it. Array.from({ length: n }) is clearer than new Array(n) anyway.
Modules and packages
Node could not find the file or package you imported, or found it and could not load it the way you asked.
ERR_MODULE_NOT_FOUNDAn import without its file extension
An import without its file extension
dies with ERR_MODULE_NOT_FOUND
greet.mjs
1export const greet = (name) => `hi ${name}`;
example.mjs
1
2
3import { greet } from "./greet";
console.log(greet("Ada"));
What node printed as it died, when the build ran it
$ node example.mjs
node:internal/modules/esm/resolve:272
throw new ERR_MODULE_NOT_FOUND(
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/app/greet' imported from /app/example.mjs
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
at moduleResolve (node:internal/modules/esm/resolve:879:10)
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
at #resolve (node:internal/modules/esm/loader:690:17)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
at ModuleJob.syncLink (node:internal/modules/esm/module_job:277:33)
at ModuleJob.link (node:internal/modules/esm/module_job:389:17) {
code: 'ERR_MODULE_NOT_FOUND',
url: 'file:///app/greet'
}
Node.js v26.7.0
What it means. The file is right there, and Node still cannot find it. ES modules in Node do not guess extensions: "./greet" means a file literally called greet. Bundlers and CommonJS both add the extension for you, which is why this surprises almost everyone once.
The fix. Write the full name: import { greet } from "./greet.mjs". Lesson 20.
ERR_MODULE_NOT_FOUNDA package that is not installed
A package that is not installed
dies with ERR_MODULE_NOT_FOUND
example.mjs
1import express from "express";
What node printed as it died, when the build ran it
$ node example.mjs
node:internal/modules/package_json_reader:301
throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'express' imported from /app/example.mjs
at Object.getPackageJSONURL (node:internal/modules/package_json_reader:301:9)
at packageResolve (node:internal/modules/esm/resolve:784:25)
at moduleResolve (node:internal/modules/esm/resolve:873:18)
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
at #resolve (node:internal/modules/esm/loader:690:17)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
at ModuleJob.syncLink (node:internal/modules/esm/module_job:277:33) {
code: 'ERR_MODULE_NOT_FOUND'
}
Node.js v26.7.0
What it means. A bare name like "express" means a package, and Node looks for it in node_modules -- in this folder, then each parent in turn. There is none. It is the same code as the entry above, with a different message: "Cannot find package".
The fix.npm install express, which this course does not run because it installs nothing from the network. Lesson 23 shows what that command changes on disk, with a local package.
SyntaxErrorImporting a name the module does not export
Importing a name the module does not export
dies with SyntaxError
maths.mjs
1
2
3export function add(a, b) {
return a + b;
}
example.mjs
1
2
3import { sum } from "./maths.mjs";
console.log(sum(1, 2));
What node printed as it died, when the build ran it
$ node example.mjs
file:///app/example.mjs:1
import { sum } from "./maths.mjs";
^^^
SyntaxError: The requested module './maths.mjs' does not provide an export named 'sum'
at #asyncInstantiate (node:internal/modules/esm/module_job:463:21)
at async ModuleJob.run (node:internal/modules/esm/module_job:561:5)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
What it means. The file was found and read, but it has no export called sum. This is checked before any code runs -- ES modules are linked first and executed second -- so it is a SyntaxError even though the syntax is fine.
The fix. Import the name that exists: import { add } from "./maths.mjs".
ERR_IMPORT_ATTRIBUTE_MISSINGImporting JSON without saying so
Importing JSON without saying so
dies with ERR_IMPORT_ATTRIBUTE_MISSING
config.json
1{ "port": 3000 }
example.mjs
1
2
3import config from "./config.json";
console.log(config.port);
What node printed as it died, when the build ran it
$ node example.mjs
node:internal/modules/esm/assert:99
throw new ERR_IMPORT_ATTRIBUTE_MISSING(url, 'type', validType);
^
TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module "file:///app/config.json" needs an import attribute of "type: json"
at validateAttributes (node:internal/modules/esm/assert:99:15)
at defaultLoadSync (node:internal/modules/esm/load:163:3)
at #loadAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:809:12)
at #loadSync (node:internal/modules/esm/loader:841:53)
at ModuleLoader.load (node:internal/modules/esm/loader:790:26)
at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:501:31)
at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:567:36)
at afterResolve (node:internal/modules/esm/loader:614:52)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:620:12)
at ModuleJob.syncLink (node:internal/modules/esm/module_job:277:33) {
code: 'ERR_IMPORT_ATTRIBUTE_MISSING'
}
Node.js v26.7.0
What it means. Node will import a JSON file, but only if you state that you expect JSON. The attribute is a security measure: without it, a file you believed was data could be served as code.
The fix. Add the attribute: import config from "./config.json" with { type: "json" }. Lesson 20.
ERR_REQUIRE_ASYNC_MODULErequire() of a module that awaits
require() of a module that awaits
dies with ERR_REQUIRE_ASYNC_MODULE
db.mjs
1export const db = await Promise.resolve({ connected: true });
example.cjs
1
2
3const { db } = require("./db.mjs");
console.log(db);
What node printed as it died, when the build ran it
$ node example.cjs
node:internal/modules/esm/module_job:314
throw new ERR_REQUIRE_ASYNC_MODULE(filename, parent, locations);
^
Error [ERR_REQUIRE_ASYNC_MODULE]: require() cannot be used on an ESM graph with top-level await. Use import() instead. To see where the top-level await comes from, use --experimental-print-required-tla.
Required module: /app/db.mjs
Require stack:
- /app/example.cjs
at ModuleJobSync.throwIfAsyncGraph (node:internal/modules/esm/module_job:314:11)
at ModuleJobSync.runSync (node:internal/modules/esm/module_job:670:10)
at ModuleLoader.importSyncForRequire (node:internal/modules/esm/loader:350:47)
at loadESMFromCJS (node:internal/modules/cjs/loader:1752:24)
at Module._compile (node:internal/modules/cjs/loader:1916:5)
at Object..js (node:internal/modules/cjs/loader:2074:10)
at Module.load (node:internal/modules/cjs/loader:1656:32)
at Module._load (node:internal/modules/cjs/loader:1448:12)
at wrapModuleLoad (node:internal/modules/cjs/loader:261:19)
at Module.require (node:internal/modules/cjs/loader:1679:12) {
code: 'ERR_REQUIRE_ASYNC_MODULE'
}
Node.js v26.7.0
What it means. Node can require an ES module now -- but require is synchronous, and a module with a top-level await cannot finish synchronously. So this one combination is refused, and the message says why.
The fix. Use await import("./db.mjs") from the CommonJS side, or move the await out of the module’s top level. Lesson 21.
ERR_PACKAGE_PATH_NOT_EXPORTEDReaching into a package past its exports
1
2
3import { hello } from "greet/internal/hello.js";
console.log(hello("Ada"));
What node printed as it died, when the build ran it
$ node example.mjs
node:internal/modules/esm/resolve:315
return new ERR_PACKAGE_PATH_NOT_EXPORTED(
^
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './internal/hello.js' is not defined by "exports" in /app/node_modules/greet/package.json imported from /app/example.mjs
at exportsNotFound (node:internal/modules/esm/resolve:315:10)
at packageExportsResolve (node:internal/modules/esm/resolve:663:9)
at packageResolve (node:internal/modules/esm/resolve:792:12)
at moduleResolve (node:internal/modules/esm/resolve:873:18)
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
at #resolve (node:internal/modules/esm/loader:690:17)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35) {
code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}
Node.js v26.7.0
What it means. A package with an "exports" field decides exactly which files outsiders may import, and everything else is private -- even though the file is right there on disk. That is what lets a library reorganise its internals without breaking you.
The fix. Import what the package exports: import { hello } from "greet". Lesson 49 writes an exports field.
Files and the system
The operating system said no. These carry a short uppercase code -- ENOENT, EACCES -- that comes straight from the OS, which is why the same codes turn up in C, Python and Go.
ENOENTA file that is not there
A file that is not there
dies with ENOENT
example.mjs
1
2
3import { readFileSync } from "node:fs";
const text = readFileSync("config.json", "utf8");
What node printed as it died, when the build ran it
$ node example.mjs
node:fs:539
return binding.readFileUtf8(path, stringToFlags(options.flag));
^
Error: ENOENT: no such file or directory, open 'config.json'
at readFileSync (node:fs:539:20)
at file:///app/example.mjs:3:14
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: 'config.json'
}
Node.js v26.7.0
What it means. "Error NO ENTry": no file or directory at that path. A relative path is resolved against the directory you ran nodefrom, not the directory the script is in, which is the usual reason a file that plainly exists is not found.
The fix. Build the path from the script’s own location: join(import.meta.dirname, "config.json"). Lesson 30.
What node printed as it died, when the build ran it
$ node example.mjs
node:fs:1651
const result = binding.mkdir(
^
Error: EEXIST: file already exists, mkdir 'out'
at mkdirSync (node:fs:1651:26)
at file:///app/example.mjs:4:1
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) {
errno: -17,
code: 'EEXIST',
syscall: 'mkdir',
path: 'out'
}
Node.js v26.7.0
What it means. The second mkdirSync finds the directory the first one made. The same code comes from opening a file with the "wx" flag, which means "create it, and fail if it is already there" -- useful precisely because of this error.
The fix.mkdirSync("out", { recursive: true }) succeeds whether or not it exists, and creates missing parents too.
ENOTEMPTYRemoving a directory that still has files
What node printed as it died, when the build ran it
$ node example.mjs
node:fs:1429
binding.rmdir(path);
^
Error: ENOTEMPTY: directory not empty, rmdir 'cache'
at rmdirSync (node:fs:1429:11)
at file:///app/example.mjs:5:1
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) {
errno: -66,
code: 'ENOTEMPTY',
syscall: 'rmdir',
path: 'cache'
}
Node.js v26.7.0
What it means.rmdir only removes empty directories. That is a safety property: deleting a directory full of files should be something you asked for explicitly.
The fix.rmSync("cache", { recursive: true, force: true }) removes it and everything in it. Mean it before you type it.
What node printed as it died, when the build ran it
$ node example.mjs
node:fs:539
return binding.readFileUtf8(path, stringToFlags(options.flag));
^
Error: EACCES: permission denied, open 'secret.txt'
at readFileSync (node:fs:539:20)
at file:///app/example.mjs:5:1
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) {
errno: -13,
code: 'EACCES',
syscall: 'open',
path: 'secret.txt'
}
Node.js v26.7.0
What it means. "Error, ACCESS". The file exists and the operating system refuses you. Here the snippet removed every permission itself; in real life it is usually a file owned by another user, or a port below 1024 that only an administrator may listen on.
The fix. Fix the permissions or the owner. Do not reach for sudo node, which gives the whole program -- and every package it imports -- full control of the machine.
What node printed as it died, when the build ran it
$ node example.mjs
node:fs:537
path = getValidatedPath(path);
^
TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string or an instance of Buffer or URL. Received undefined
at readFileSync (node:fs:537:14)
at file:///app/example.mjs:4:1
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) {
code: 'ERR_INVALID_ARG_TYPE'
}
Node.js v26.7.0
What it means. Node’s own functions check their arguments and say precisely what they got. The message names the argument ("path"), what it accepts, and what arrived -- undefined, from a property that was never set.
The fix. Trace where the value should have come from. Here: settings.path does not exist.
ERR_ACCESS_DENIEDReading outside what the permission model allows
Reading outside what the permission model allows
dies with ERR_ACCESS_DENIED
example.mjs
1
2
3import { readFileSync } from "node:fs";
readFileSync("/etc/hosts", "utf8");
What node printed as it died, when the build ran it
node:fs:539
return binding.readFileUtf8(path, stringToFlags(options.flag));
^
Error: Access to this API has been restricted. Use --allow-fs-read to manage permissions.
at readFileSync (node:fs:539:20)
at file:///app/example.mjs:3:1
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) {
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: '/etc/hosts'
}
Node.js v26.7.0
What it means. Run with --permission, Node itself refuses file, network and child-process access unless you granted it with an --allow-* flag. This process was only allowed to read its own script, so reading anything else is denied by Node before the operating system is even asked.
The fix. Grant exactly what the program needs: --allow-fs-read=./data/. Lesson 40.
Async
Something failed after the code that started it had already moved on.
ErrorA rejected promise nobody handled
A rejected promise nobody handled
dies with Error
example.mjs
1
2
3
4
5
6async function loadUser(id) {
throw new Error(`no user with id ${id}`);
}
loadUser(7);
console.log("carried on");
What node printed as it died, when the build ran it
$ node example.mjs
carried on
file:///app/example.mjs:2
throw new Error(`no user with id ${id}`);
^
Error: no user with id 7
at loadUser (file:///app/example.mjs:2:9)
at file:///app/example.mjs:5:1
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
What it means. The function rejected, nothing had a .catch or await on it, and Node’s default is to treat that as a crash: the process dies with exit code 1. Note that "carried on" printed first -- the rejection is only noticed once the current code has finished.
The fix.await the call inside a try, or attach .catch(). Lesson 28.
ERR_UNHANDLED_REJECTIONRejecting with something that is not an Error
Rejecting with something that is not an Error
dies with ERR_UNHANDLED_REJECTION
example.mjs
1Promise.reject("database unreachable");
What node printed as it died, when the build ran it
$ node example.mjs
node:internal/process/promises:322
new UnhandledPromiseRejection(reason);
^
UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "database unreachable".
at throwUnhandledRejectionsMode (node:internal/process/promises:322:7)
at processPromiseRejections (node:internal/process/promises:405:17)
at process.processTicksAndRejections (node:internal/process/task_queues:106:37) {
code: 'ERR_UNHANDLED_REJECTION'
}
Node.js v26.7.0
What it means. The same crash as above, but the rejection reason is a plain string, so there is no stack to show and Node has to wrap it in an error of its own. You lose the location entirely -- the strongest argument for always rejecting and throwing Error objects.
The fix.throw new Error("database unreachable"), never a bare string.
ABORT_ERRAn operation that was cancelled
An operation that was cancelled
dies with ABORT_ERR
example.mjs
1
2
3
4
5
6import { setTimeout as sleep } from "node:timers/promises";
const controller = new AbortController();
setTimeout(() => controller.abort(), 20);
await sleep(10_000, null, { signal: controller.signal });
What node printed as it died, when the build ran it
$ node example.mjs
node:timers/promises:49
reject(new AbortError(undefined, { cause: signal?.reason }));
^
AbortError: The operation was aborted
at Timeout.cancelListenerHandler (node:timers/promises:49:12)
at [nodejs.internal.kHybridDispatch] (node:internal/event_target:856:20)
at AbortSignal.dispatchEvent (node:internal/event_target:789:26)
at runAbort (node:internal/abort_controller:554:10)
at abortSignal (node:internal/abort_controller:518:3)
at AbortController.abort (node:internal/abort_controller:573:5)
at Timeout._onTimeout (file:///app/example.mjs:4:29)
at listOnTimeout (node:internal/timers:685:17)
at process.processTimers (node:internal/timers:618:7) {
code: 'ABORT_ERR',
[cause]: DOMException [AbortError]: This operation was aborted
at new DOMException (node:internal/per_context/domexception:79:18)
at AbortController.abort (node:internal/abort_controller:572:18)
at Timeout._onTimeout (file:///app/example.mjs:4:29)
at listOnTimeout (node:internal/timers:685:17)
at process.processTimers (node:internal/timers:618:7)
}
Node.js v26.7.0
What it means. An AbortSignal is how you cancel something in Node -- a timer, a fetch, a stream. When the signal fires, the operation rejects with an AbortError. Here nothing caught it, so the cancellation became a crash.
The fix. Catch it and check err.name === "AbortError", since a cancellation you asked for is usually not a failure. Lesson 28.
What node printed as it died, when the build ran it
$ node example.mjs
file:///app/example.mjs:3
throw new Error("thrown later");
^
Error: thrown later
at Timeout._onTimeout (file:///app/example.mjs:3:11)
at listOnTimeout (node:internal/timers:685:17)
at process.processTimers (node:internal/timers:618:7)
Node.js v26.7.0
What it means. The try finished long before the callback ran, so it cannot catch what the callback throws. Nothing is on the stack below the callback except Node’s own timer machinery, which the frames show.
The fix. Handle errors inside the callback itself, or use promises, where await inside try does catch them. Lesson 24.
Network and HTTP
Every one of these was produced against a server the snippet started itself on 127.0.0.1. Nothing on this page touches the internet.
EADDRINUSEA port something else is already using
A port something else is already using
dies with EADDRINUSE
example.mjs
1
2
3
4
5
6import { createServer } from "node:http";
const first = createServer().listen(0, "127.0.0.1", () => {
const { port } = first.address();
createServer().listen(port, "127.0.0.1");
});
What node printed as it died, when the build ran it
$ node example.mjs
node:events:505
throw er; // Unhandled 'error' event
^
Error: listen EADDRINUSE: address already in use 127.0.0.1:53140
at Server.setupListenHandle [as _listen2] (node:net:2324:16)
at listenInCluster (node:net:2433:12)
at node:net:2662:7
at process.processTicksAndRejections (node:internal/process/task_queues:90:21)
Emitted 'error' event on Server instance at:
at emitErrorNT (node:net:2412:8)
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
code: 'EADDRINUSE',
errno: -48,
syscall: 'listen',
address: '127.0.0.1',
port: 53140
}
Node.js v26.7.0
What it means. Only one process can listen on a given port. Usually the other one is a copy of your own server you forgot was still running in another terminal. Note it arrives as an unhandled 'error' event rather than a thrown error -- servers report problems through events.
The fix. Stop the other process, or choose another port. server.on("error", ...) lets you report it properly instead of crashing.
ECONNREFUSEDConnecting to a port nothing is listening on
Connecting to a port nothing is listening on
dies with ECONNREFUSED
example.mjs
1
2
3
4
5
6
7import { createServer, connect } from "node:net";
// Find a free port, then close it, so nothing is listening there.
const probe = createServer().listen(0, "127.0.0.1", () => {
const { port } = probe.address();
probe.close(() => connect(port, "127.0.0.1"));
});
What node printed as it died, when the build ran it
$ node example.mjs
node:events:505
throw er; // Unhandled 'error' event
^
Error: connect ECONNREFUSED 127.0.0.1:53141
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:2017:16)
Emitted 'error' event on Socket instance at:
at emitErrorNT (node:internal/streams/destroy:170:8)
at emitErrorCloseNT (node:internal/streams/destroy:129:3)
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
errno: -61,
code: 'ECONNREFUSED',
syscall: 'connect',
address: '127.0.0.1',
port: 53141
}
Node.js v26.7.0
What it means. The machine is there, and nothing on it is accepting connections on that port, so the connection is refused immediately. The server is not running, or it is on a different port.
The fix. Start the server, and check the port. From fetch the same thing appears as TypeError: fetch failed with ECONNREFUSED in its cause; lesson 38 unpacks that.
ERR_HTTP_HEADERS_SENTSetting a header after the response was sent
What node printed as it died, when the build ran it
$ node example.mjs
node:_http_outgoing:684
throw new ERR_HTTP_HEADERS_SENT('set');
^
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at ServerResponse.setHeader (node:_http_outgoing:684:11)
at Server.<anonymous> (file:///app/example.mjs:5:7)
at Server.emit (node:events:514:20)
at parserOnIncoming (node:_http_server:1394:12)
at HTTPParser.parserOnHeadersComplete (node:_http_common:125:17) {
code: 'ERR_HTTP_HEADERS_SENT'
}
Node.js v26.7.0
What it means. Headers go out before the body, so once res.end() has sent the response there is no way to add one. In real code it is almost always two code paths that both respond -- an error handler and the normal path, say, with a missing return between them.
The fix.return res.end(...), so nothing after it in the handler runs. Lesson 36.
ERR_INVALID_URLA URL that is not one
A URL that is not one
dies with ERR_INVALID_URL
example.mjs
1
2const base = process.env.API_URL; // not set
const url = new URL("/users", base);
What node printed as it died, when the build ran it
$ node example.mjs
node:internal/url:862
href = bindingUrl.parse(input, base, true);
^
TypeError: Invalid URL
at new URL (node:internal/url:862:25)
at file:///app/example.mjs:2:13
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) {
code: 'ERR_INVALID_URL',
input: '/users'
}
Node.js v26.7.0
What it means.new URL refuses anything it cannot parse, and a relative path needs a base. Here the base came from an environment variable nobody set, so it is undefined, and "/users" on its own is not a URL.
The fix. Validate configuration at startup, not on first use. Lesson 48.
Events and streams
The error event, which is special, and the stream rules that are easy to break once.
ERR_UNHANDLED_ERROREmitting 'error' with no listener
Emitting 'error' with no listener
dies with ERR_UNHANDLED_ERROR
example.mjs
1
2
3
4import { EventEmitter } from "node:events";
const queue = new EventEmitter();
queue.emit("error", "worker crashed");
What node printed as it died, when the build ran it
$ node example.mjs
node:events:477
const err = new ERR_UNHANDLED_ERROR(stringifiedEr);
^
Error [ERR_UNHANDLED_ERROR]: Unhandled error. ('worker crashed')
at getUnhandledErrorException (node:events:477:15)
at EventEmitter.emit (node:events:502:16)
at file:///app/example.mjs:4:7
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) {
code: 'ERR_UNHANDLED_ERROR',
context: 'worker crashed'
}
Node.js v26.7.0
What it means. The 'error' event is special: if nothing is listening for it, emitting it throws. Every other event with no listener is silently ignored. Because the value emitted here is a string rather than an Error, Node wraps it in ERR_UNHANDLED_ERROR.
The fix. Always attach an 'error' listener to anything that can fail -- servers, streams, sockets. Lesson 33.
ERR_STREAM_WRITE_AFTER_ENDWriting to a stream after ending it
Writing to a stream after ending it
dies with ERR_STREAM_WRITE_AFTER_END
example.mjs
1
2
3
4
5import { createWriteStream } from "node:fs";
const out = createWriteStream("log.txt");
out.end("last line\n");
out.write("one more\n");
What node printed as it died, when the build ran it
$ node example.mjs
node:events:505
throw er; // Unhandled 'error' event
^
Error [ERR_STREAM_WRITE_AFTER_END]: write after end
at _write (node:internal/streams/writable:491:11)
at Writable.write (node:internal/streams/writable:512:10)
at file:///app/example.mjs:5:5
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Emitted 'error' event on WriteStream instance at:
at emitErrorNT (node:internal/streams/destroy:170:8)
at emitErrorCloseNT (node:internal/streams/destroy:129:3)
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
code: 'ERR_STREAM_WRITE_AFTER_END'
}
Node.js v26.7.0
What it means.end() means "this is everything". A write after it has nowhere to go. It is reported through the stream’s 'error' event, and there is no listener, so the process dies.
The fix. Do all the writing first, then call end() exactly once. Lesson 34.
Data
Text that is not the JSON you expected, and values JSON cannot represent.
SyntaxErrorText that is not JSON
Text that is not JSON
dies with SyntaxError
example.mjs
1
2const body = "{ name: 'Ada' }";
const user = JSON.parse(body);
What node printed as it died, when the build ran it
$ node example.mjs
<anonymous_script>:1
{ name: 'Ada' }
^
SyntaxError: Expected property name or '}' in JSON at position 2 (line 1 column 3)
at JSON.parse (<anonymous>)
at file:///app/example.mjs:2:19
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
What it means. That is a JavaScript object literal, not JSON. JSON requires double quotes around every key and every string. The message gives the exact position where parsing failed.
The fix.{ "name": "Ada" }. And wrap any JSON.parse of outside input in try, because outside input is exactly where this comes from. Lesson 16.
What node printed as it died, when the build ran it
$ node example.mjs
file:///app/example.mjs:4
JSON.stringify(node);
^
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
--- property 'parent' closes the circle
at JSON.stringify (<anonymous>)
at file:///app/example.mjs:4:6
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
What it means. JSON is a tree, and this object is a loop: node.parent is node. The message draws the loop for you, property by property.
The fix. Serialise only the data you mean to -- JSON.stringify({ name: node.name }) -- or pass a replacer that drops the back-reference.
What node printed as it died, when the build ran it
$ node example.mjs
file:///app/example.mjs:3
JSON.stringify(order);
^
TypeError: Do not know how to serialize a BigInt
at JSON.stringify (<anonymous>)
at file:///app/example.mjs:3:6
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
What it means. JSON numbers are ordinary doubles, and a BigInt exists precisely because it may not fit in one. Rather than silently round it, JSON.stringify refuses.
The fix. Send it as a string: { ...order, id: order.id.toString() }. Lesson 5 shows why the rounding would have been real.
How to read any of them. Find the line that starts with the error's name --
TypeError: ..., or Error [ERR_...]: .... That line is the problem. The
first at line under it that points into your file, rather than
node:internal, is where it happened. Above the name, Node prints that line of your
source with a ^ under the exact spot. And a code in capitals is the thing to search
for, and the thing to test in a catch: err.code === "ENOENT".
Newsletter
New writing, when it lands.
Occasional notes on shipping software with AI agents. No schedule, no filler, unsubscribe in one click.