Stage 0 — Pre-proposal
TBD
A standardized code property would enable:
-
Precise programmatic error handling. Application code can branch on specific error conditions without depending on message strings:
try { await db.connect(); } catch (e) { if (e.code === 'ERR_CONNECTION_REFUSED') { // Retry with backoff } }
-
Stable error contracts across versions. An error code is a machine-readable identifier that can be documented, versioned, and relied upon — unlike messages, which are implementation-defined prose.
-
Cross-realm error identification. Unlike
instanceof, a string.codeproperty survives structured cloning, serialization, and cross-realm transfer. -
Improved error reporting and telemetry. Aggregating errors by
.codein monitoring systems (Sentry, Datadog, etc.) is more reliable and useful than aggregating by message, which varies across engines and versions. -
Error translation and i18n. With a stable code, error messages can be localized or mapped to user-facing text without losing machine-readability.
-
Ecosystem alignment. Providing a standard property that the ecosystem already uses would reduce fragmentation and give library authors a blessed pattern to follow.
-
Better developer experience. Documentation and tooling can reference error codes. A developer can search for
ERR_INVALID_ARG_TYPEand find definitive documentation, whereas searching for a message string is unreliable.
The JavaScript ecosystem has overwhelmingly and independently adopted error.code
as the solution. This section surveys the landscape.
The most mature implementation. Node.js has used string .code properties on errors
since v8.0 (2017), with hundreds of documented codes.
- Type:
string(instance property) - Convention:
ERR_*prefix for Node.js errors (e.g.,ERR_INVALID_ARG_TYPE,ERR_BUFFER_OUT_OF_BOUNDS,ERR_HTTP2_INVALID_HEADER_VALUE). POSIX codes for system errors (ENOENT,EACCES,ECONNREFUSED). - Scope: 200+ documented
ERR_*codes organized by subsystem, plus POSIX and OpenSSL codes. - Architecture: Internal
NodeErrorAbstractionclasses extend native types (TypeError,RangeError, etc.) and set.codein the constructor. - Design choice: Node.js deliberately chose strings over numbers so that error handling code is self-documenting without looking up magic numbers.
- Stability commitment: Error codes are part of the stable API. Removing or changing the semantics of a code is a breaking change.
// Node.js error with .code
const fs = require('fs');
try {
fs.readFileSync('/nonexistent');
} catch (e) {
e.code; // 'ENOENT'
e.message; // "ENOENT: no such file or directory, open '/nonexistent'"
}Deno mirrors Node.js .code exactly in its node:* compatibility layer:
- Node compat: Uses
ERR_*string codes, identical to Node.js. - Native APIs: Uses a class hierarchy instead (
Deno.errors.NotFound,Deno.errors.PermissionDenied), relying oninstanceofchecks. POSIX errno strings (e.g.,ENOENT,ECONNREFUSED) are attached as.codeon I/O errors via the underlying OS error, but this is not documented as a public API.
The fact that Deno adopted Node.js error codes wholesale for its compat layer
demonstrates that the pattern is essential for interoperability. Deno's native
choice of instanceof-based error classes highlights the limitation of that
approach — it doesn't survive structured cloning or cross-realm transfer, and
requires importing specific error constructors.
// Running in Deno
let e = new Deno.errors.PermissionDenied()
console.log(e instanceof Deno.errors.PermissionDenied) // true
console.log(e.name) // "PermissionDenied"
// The details of the error do not survive structured cloning
let clone = structuredClone(e)
console.log(clone instanceof Deno.errors.PermissionDenied) // false
console.log(clone.name) // "Error"
// Does not survive postMessage either
const { port1, port2 } = new MessageChannel();
port1.postMessage(e);
port2.onmessage = (event) => {
const received = event.data;
console.log(received instanceof Deno.errors.PermissionDenied) // false
console.log(received.name) // "Error"
};Deno also does not implement DOMException as structured cloneable, so it loses
all type information, including the modified .name property when cloned.
Bun mirrors Node.js .code for all node:* module errors, and has extended
the same convention to its own native APIs:
- Same
ERR_*convention and POSIX codes for system errors. - Bun-native APIs use original
ERR_*codes: e.g.,ERR_POSTGRES_*for the built-in Postgres client,ERR_REDIS_*for Redis, andERR_S3_*for S3.
Bun's decision to adopt ERR_* codes for its own non-Node APIs — rather than
inventing a separate error identification scheme — is strong evidence that
.code is the natural extension point for JavaScript errors.
Cloudflare Workers also mirror Node.js error codes for node:* module errors
- Same
ERR_*convention for compatibility. - Native APIs throw standard
Errorwithout custom codes.
DOMException has both a legacy numeric .code and a modern string .name:
.code(number): Deprecated. Legacy constants likeNOT_FOUND_ERR = 8,NOT_SUPPORTED_ERR = 9. Returns0for all newer error types added after ~2012..name(string): The modern identifier. PascalCase:"NotFoundError","AbortError","DataCloneError".
The web platform's explicit deprecation of numeric .code in favor of string
.name is a clear signal: string-based error identification is the right path.
- Type:
number - Codes:
MEDIA_ERR_ABORTED = 1,MEDIA_ERR_NETWORK = 2,MEDIA_ERR_DECODE = 3,MEDIA_ERR_SRC_NOT_SUPPORTED = 4
- Type:
number - Codes:
PERMISSION_DENIED = 1,POSITION_UNAVAILABLE = 2,TIMEOUT = 3
- Inherits DOMException (
.code+.name) but adds a string.errorDetailproperty ("sdp-syntax-error","dtls-failure", etc.) for domain-specific identification.
Older Web APIs used numeric codes. Newer ones use strings. The platform has moved toward string-based identification.
The following major libraries independently adopted error.code:
| Library | .code type |
Convention |
|---|---|---|
| axios | string |
ERR_NETWORK, ERR_CANCELED, ETIMEDOUT |
| Firebase | string |
"auth/user-not-found", "storage/not-found" |
| Stripe | string |
"card_declined", "rate_limit" |
| Prisma | string |
"P2002", "P2025", "P1001" |
| pg (Postgres) | string |
SQLSTATE codes: "23505", "42P01" |
| mysql2 | string |
"ER_DUP_ENTRY", "ER_ACCESS_DENIED_ERROR" |
| MongoDB/mongoose | number |
MongoDB server codes: 11000 |
| @grpc/grpc-js | number |
gRPC status codes: 5 (NOT_FOUND) |
| AWS SDK v3 | string |
"AccessDenied", "NoSuchBucket" |
| Zod | string |
"invalid_type", "too_small" |
String codes dominate. Numeric codes appear only where an upstream protocol defines them (gRPC, MongoDB).
| Language | Mechanism | Type |
|---|---|---|
| Python | Exception class hierarchy + OSError.errno |
class + int |
| Rust | std::io::ErrorKind enum + .raw_os_error() |
enum + i32 |
| Go | Sentinel values (os.ErrNotExist) + errors.Is() |
value identity |
| Java | Exception hierarchy + SQLException.getSQLState() |
class + String |
| C#/.NET | Exception hierarchy + Exception.HResult |
class + int |
Most languages use type hierarchies as the primary mechanism, with optional
string/numeric codes for interop with external systems (OS, databases, protocols).
JavaScript lacks a practical type hierarchy for this purpose (limited built-in
subtypes, cross-realm issues with instanceof), making a property-based approach
more appropriate.
error.name already exists and defaults to the constructor name ("TypeError",
"RangeError", etc.). However:
.nameis coarse-grained — all TypeErrors share the same.name..codeprovides fine-grained identification within an error type.- They are complementary:
.namesays what kind of error;.codesays which specific error condition. - Overwriting
.nameto encode specific conditions conflates two concerns and breaksinstanceof-based expectations.
DOMException is a cautionary example of what happens when .name is repurposed to
carry specific error identity. Every DOMException instance has its .name set to a
specific error string like "NotFoundError" or "AbortError" rather than
"DOMException". This means:
instanceofand.namedisagree.err instanceof DOMExceptionistrue, buterr.nameis"AbortError", not"DOMException". This breaks the fundamental expectation that.namereflects the constructor/type.error.namebecomes load-bearing for dispatch. Code must switch on.nameto distinguish DOMException subtypes, making.namea de facto error code while still nominally being the "type name." This is exactly what.codeshould be for.- It forecloses subclassing. Because
.namealready carries the specific error identity, there is no room for a DOMException subclass to have its own.namewithout losing the error identity, and no room for.nameto reflect the actual class hierarchy. - Stack traces and logging are misleading. A logged
DOMExceptionshows its.nameas"AbortError", which looks like a separate error class that doesn't exist. Developers search for anAbortErrorconstructor and find nothing (or find thatAbortErroris just aDOMExceptionwith a specific.name).
Had .code existed as a standard property, DOMException could have used
{ code: "AbortError" } while keeping .name as "DOMException", preserving the
natural relationship between .name, instanceof, and the class hierarchy.
Extend the Error constructor options bag (introduced in ES2022 for cause) to
accept a code property. This applies to Error, all NativeError types
(TypeError, RangeError, etc.), AggregateError, and SuppressedError:
new Error("something went wrong", { code: "ERR_SOMETHING" })
new TypeError("expected string", { code: "ERR_INVALID_ARG_TYPE", cause: original })The .code property would be:
- Defined on instances, not on
Error.prototype - Type: any value (not restricted to strings, consistent with
cause) - Default: property is not present when not provided (
'code' in errisfalse), consistent withcause - Enumerable:
false(consistent withcauseandmessage) - Writable:
true(consistent with other Error properties) - Configurable:
true
While strings are the dominant convention in the ecosystem (Node.js, axios, Firebase,
Stripe, etc.), the spec should not constrain the type. Several major libraries use
numeric codes (gRPC, MongoDB, TypeScript diagnostics), and cause already
established the precedent of accepting any value without type restriction.
The ecosystem strongly favors strings for the reasons outlined in the prior art survey — self-documenting, no lookup tables, no collision risk — but this is best left as a convention rather than a language-level constraint.
- Backward compatibility: existing
new Error("msg")calls should work unchanged. - Not all errors have meaningful codes (e.g., ad-hoc
throw new Error("bug")). - Follows the
causeprecedent, which is also absent when not provided.
Error.cause(ES2022): Established the options bag pattern on theErrorconstructor. This proposal extends that same bag withcode.- Error Stacks (Stage 1): Focuses on standardizing
error.stack. Orthogonal but complementary — stacks could include codes. Related proposals will make use of the options bag for other metadata. Error.isError(Stage 2): Cross-realm error identification. Complementary —.codeprovides fine-grained identification within a confirmed error.
No more than error.message and error.name already do in practice. The key
difference is that .code is explicitly intended as a machine-readable
identifier, ideally with stability guarantees, whereas .message is
human-readable prose. A .code is semantically equivalent to a discriminant in
a tagged union — it just uses a string (typically) instead of a type tag.
They already do — that's the current situation. Standardizing the property
doesn't require standardizing the values. It provides a blessed location for
codes (instead of ad-hoc properties like .errno, .errorCode, .errCode, etc.)
and enables tooling to be built around a single convention.
No. The proposal does not prescribe any specific codes or taxonomies. It simply
provides a standard property for libraries and applications to use if they choose
to. The ecosystem can evolve organically, and popular codes will emerge as de
facto standards (e.g., ERR_INVALID_ARG_TYPE in Node.js).
Symbols would prevent collisions but sacrifice the key advantages of string codes:
serialization, logging, telemetry aggregation, human readability, and cross-realm
transfer. The Node.js ecosystem has demonstrated that string codes with prefix
conventions (ERR_*) are practical and collision-resistant.
But since the code value can be any type, libraries and applications can use symbols if they prefer - the proposal does not preclude that.
In theory, yes — a class hierarchy can encode any error taxonomy. In practice:
instanceofbreaks across realms.- The built-in error hierarchy is too shallow (only ~7 types).
- Creating deep class hierarchies for every error condition is ergonomically heavy.
- Error subclasses cannot be pattern-matched in
catch(nocatch (e if ...)in standard JS). - The ecosystem has already voted with its feet:
.codeon instances. - Structured cloning and cross-realm transfer of errors is common (e.g.,
postMessage), and classes don't survive that.
We see these limitations demonstrated in Deno's namespace of Deno.errors.*
classes, which cannot be reliably identified across cloning boundaries, and
variable runtime support of structuredClone, etc.