Create typed, structured error classes from simple definitions.
Lets you define errors declaratively and get back proper classes with:
- Typed error codes — catch and handle errors by code, not by sniffing message strings
- HTTP status codes — errors carry their status, so your error handler doesn't need a mapping table
- Message templates — parameterised messages with compile-time checking of required params
- PascalCase names —
NOT_FOUNDbecomesNotFoundin stack traces automatically - Clean stack traces — the error points at your throw site, not the class internals
- Standard
causechaining — wrap underlying errors using the ES2022causeoption
I found myself using similar patterns for error handling in multiple projects, and this module encapsulates the common boilerplate into a simple, reusable API.
npm install custom-error-creator
import { createErrorClass } from "custom-error-creator";
const NotFound = createErrorClass({
code: "NOT_FOUND",
message: "Resource {resource} not found",
status: 404,
});
throw new NotFound({ resource: "User" });
// NotFound: Resource User not found
// at handler (/app/routes.ts:12:9)Creates a single error class from a definition.
const ValidationError = createErrorClass({
code: "VALIDATION_ERROR",
message: "{field} is invalid: {reason}",
status: 400,
});status is optional. Omit it for errors that don't map to an HTTP status — the
instance then has no status property at all (reading .status is a
compile-time error, and "status" in err is false at runtime):
const ConfigError = createErrorClass({
code: "CONFIG_ERROR",
message: "Invalid config",
});
const err = new ConfigError();
err.status; // ❌ compile error — no such propertyTo recognise errors regardless of status, use the looser
isErrorWithCode guard, which checks only code.
Creates multiple error classes at once, returned as an object keyed by code.
import { createErrorClassesByCode } from "custom-error-creator";
const errors = createErrorClassesByCode([
{ code: "NOT_FOUND", message: "Resource {resource} not found", status: 404 },
{ code: "UNAUTHORIZED", message: "Access denied", status: 401 },
{
code: "VALIDATION_ERROR",
message: "{field} is invalid: {reason}",
status: 400,
},
]);
throw new errors.NOT_FOUND({ resource: "User" });
throw new errors.UNAUTHORIZED();
throw new errors.VALIDATION_ERROR({ field: "email", reason: "too short" });Creates multiple error classes at once, returned as an object keyed by PascalCase name.
import { createErrorClassesByName } from "custom-error-creator";
const errors = createErrorClassesByName([
{ code: "NOT_FOUND", message: "Resource {resource} not found", status: 404 },
{ code: "UNAUTHORIZED", message: "Access denied", status: 401 },
]);
throw new errors.NotFound({ resource: "User" });
throw new errors.Unauthorized();Type guard for errors created by this module that carry a numeric status.
Narrows to the exported CustomError type. Duck-typed on code + status, so
it also matches manually augmented errors with those properties.
A looser alternative to isCustomError when you only want to check against
error.code. Narrows to the exported ErrorWithCode type. It checks code
alone and ignores status, so it matches every error this module creates (with
or without a status) as well as unrelated errors that carry a string code,
such as Node's system errors (ENOENT, etc.). Narrow further on error.code
when you need to be specific.
The constructor is flexible depending on whether the message template has parameters.
When the default message contains {param} placeholders, the params object is
required:
const NotFound = createErrorClass({
code: "NOT_FOUND",
message: "Resource {resource} not found",
status: 404,
});
// Params required — typed and checked at compile time
new NotFound({ resource: "User" });
new NotFound({ resource: "User" }, { cause: underlyingError });
// Custom message — params become optional and untyped
new NotFound("Thing not found");
new NotFound("{thing} is gone", { thing: "Widget" });
new NotFound("Gone", { cause: underlyingError });
new NotFound("{x} missing", { x: "y" }, { cause: underlyingError });const Unauthorized = createErrorClass({
code: "UNAUTHORIZED",
message: "Access denied",
status: 401,
});
new Unauthorized();
new Unauthorized("Custom message");
new Unauthorized("Custom message", { cause: underlyingError });When a {param} template isn't expressive enough, pass a function as the
message. Its parameter must be an object type, which defines the typed params
required by the constructor, and you control the formatting:
const TooMany = createErrorClass({
code: "TOO_MANY_ITEMS",
message: (params: { items: number[] }) =>
`Found ${params.items.length} items, expected fewer`,
status: 400,
});
// Params required — typed as { items: number[] } from the function signature
new TooMany({ items: [1, 2, 3] });
new TooMany({ items: [1, 2, 3] }, { cause: underlyingError });
new TooMany({ items: "nope" }); // ❌ wrong param type, caught at compile timeThe parameter must be an object — (n: number) => ... is a compile-time error,
since the constructor passes params as an object.
Declaring the parameter as optional (or giving it a default) makes params
optional in the constructor too. The function is always called with an object
({} when no params are passed), so a default like = {} works as expected:
const DocError = createErrorClass({
code: "DOC_ERROR",
message: ({ docType }: { docType?: string } = {}) =>
docType ? `${docType} error` : "other error message",
status: 400,
});
new DocError(); // "other error message"
new DocError({ docType: "observation" }); // "observation error"
new DocError({ cause: underlyingError }); // cause without paramsA zero-argument function behaves like an error without template parameters:
const Unauthorized = createErrorClass({
code: "UNAUTHORIZED",
message: () => "Access denied",
status: 401,
});
new Unauthorized();The optional custom message passed to the constructor is always a plain string
(new TooMany("Custom message")) — the function only produces the default
message.
As with {param} templates, cause is a reserved parameter name: a function
message whose params include a cause key is a compile-time error. This keeps
cause meaning only "the error's cause" (passed as the second constructor
argument), with no shadowing. Because a function's parameter types are erased at
runtime, this is enforced at compile time only — unlike the {cause} template
check, which also throws at runtime.
Every error instance has the standard Error properties plus:
const err = new NotFound({ resource: "User" });
err.message; // "Resource User not found"
err.name; // "NotFound"
err.code; // "NOT_FOUND"
err.status; // 404 (the property is absent if the definition omits `status`)
err.stack; // stack trace pointing at the throw site
err.cause; // underlying error, if providedError classes also expose code and name as static properties, useful for
comparisons without instantiating:
const NotFound = createErrorClass({
code: "NOT_FOUND",
message: "Resource {resource} not found",
status: 404,
});
NotFound.code; // "NOT_FOUND"
NotFound.name; // "NotFound"try {
await getUser(id);
} catch (err) {
if (err.code === "NOT_FOUND") {
// handle missing resource
}
}try {
await getUser(id);
} catch (err) {
if (err instanceof NotFound) {
// handle missing resource
}
}Use the standard cause option to chain underlying errors:
try {
await db.query("SELECT ...");
} catch (err) {
throw new NotFound({ resource: "User" }, { cause: err });
}The cause is set using the native Error constructor option (ES2022), so it
behaves identically to new Error("msg", { cause }) — non-enumerable and
compatible with all standard tooling.
The parameter name cause is reserved and cannot be used as a message
parameter, so it only ever refers to the error's cause (the second constructor
argument). For string templates this is enforced at both compile time and
runtime; for function messages, whose param types are erased at runtime, it is
enforced at compile time only.
// ❌ Compile error at definition time + runtime throw
const Bad = createErrorClass({
code: "BAD",
message: "Failed because {cause}",
status: 500,
});
// ❌ Compile error at definition time (function params)
const AlsoBad = createErrorClass({
code: "BAD",
message: (params: { cause: string }) => `Failed because ${params.cause}`,
status: 500,
});Template parameters are fully type-checked when using the default message:
const NotFound = createErrorClass({
code: "NOT_FOUND",
message: "Resource {resource} not found",
status: 404,
});
new NotFound({ resource: "User" }); // ✅
new NotFound({ resorce: "User" }); // ❌ typo caught at compile time
new NotFound({}); // ❌ missing required param
new NotFound(); // ❌ params requiredInstance properties are also typed:
const err = new NotFound({ resource: "User" });
err.code; // type: "NOT_FOUND"
err.status; // type: 404
err.name; // type: "NotFound"MIT