Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export type ErrorConstructor<Def extends ErrorDefinition> = (HasParams<
}
: {
new (): ErrorInstance<Def>;
new (opts: ErrorOpts): ErrorInstance<Def>;
new (message: string): ErrorInstance<Def>;
new (message: string | undefined, opts: ErrorOpts): ErrorInstance<Def>;
}) & {
Expand Down
13 changes: 10 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ export function createErrorClass(definition) {

validateMessage(code, defaultMessage);

const hasTemplateParams = /\{\w+\}/.test(defaultMessage);

const ErrorKlass = class extends Error {
code = code;
status = status;
Expand All @@ -155,9 +157,14 @@ export function createErrorClass(definition) {
let params, cause;

if (typeof messageOrParams === "object" && messageOrParams !== null) {
// First arg is params object: new Err(params) or new Err(params, opts)
params = messageOrParams;
cause = paramsOrOpts?.cause;
if (!hasTemplateParams && "cause" in messageOrParams) {
// No-param error: first arg is ErrorOpts
cause = messageOrParams.cause;
} else {
// First arg is params object: new Err(params) or new Err(params, opts)
params = messageOrParams;
cause = paramsOrOpts?.cause;
}
} else if (typeof paramsOrOpts === "object" && paramsOrOpts !== null) {
if (opts !== undefined) {
// Three-arg form: new Err("msg", params, opts)
Expand Down
27 changes: 27 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,33 @@ expectError(
expectType<"NOT_FOUND">(NotFound.code);
expectType<"NotFound">(NotFound.name);

// ──────────────────────────────────────────────
// ErrorOpts as first parameter (no-param errors)
// ──────────────────────────────────────────────

const SimpleErr = createErrorClass({
code: "SIMPLE",
message: "Something failed",
status: 500,
});

// ErrorOpts as first arg — should compile for no-param errors
new SimpleErr({ cause: new Error("root") });
new SimpleErr({ cause: "string cause" });
new SimpleErr({ cause: null });
new SimpleErr({ cause: 42 });

// Empty object also valid (cause is optional in ErrorOpts)
new SimpleErr({});

// Should NOT compile for parameterized errors (object treated as params, wrong shape)
const ParamErrOpts = createErrorClass({
code: "PARAM_ERR",
message: "Missing {field}",
status: 400,
});
expectError(new ParamErrOpts({ cause: new Error() }));

// ──────────────────────────────────────────────
// Non-string template parameters
// ──────────────────────────────────────────────
Expand Down
51 changes: 51 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,57 @@ describe("createErrorClass", () => {
});
});

describe("ErrorOpts as first argument for no-param errors", () => {
const Err = createErrorClass({
code: "SIMPLE",
message: "Something failed",
status: 500,
});

it("accepts { cause } as first argument", () => {
const cause = new Error("root cause");
const err = new Err({ cause });
assert.equal(err.message, "Something failed");
assert.equal(err.cause, cause);
});

it("accepts { cause: null } preserving falsy cause", () => {
const err = new Err({ cause: null });
assert.equal(err.message, "Something failed");
assert.equal(err.cause, null);
});

it("accepts { cause: undefined } — no cause set", () => {
const err = new Err({ cause: undefined });
assert.equal(err.message, "Something failed");
assert.equal(err.cause, undefined);
});

it("accepts {} — no cause set, uses default message", () => {
const err = new Err({});
assert.equal(err.message, "Something failed");
assert.equal(err.cause, undefined);
});

it("does not affect parameterized errors", () => {
const ParamErr = createErrorClass({
code: "PARAM_ERR",
message: "Missing {field}",
status: 400,
});
// For parameterized errors, first object arg is always params
const err = new ParamErr({ field: "name" });
assert.equal(err.message, "Missing name");
assert.equal(err.cause, undefined);
});

it("cause via opts is non-enumerable", () => {
const err = new Err({ cause: new Error("root") });
const descriptor = Object.getOwnPropertyDescriptor(err, "cause");
assert.equal(descriptor.enumerable, false);
});
});

describe("non-string template parameters", () => {
it("interpolates number params", () => {
const Err = createErrorClass({
Expand Down