diff --git a/index.d.ts b/index.d.ts index 1aa8452..f45d0cb 100644 --- a/index.d.ts +++ b/index.d.ts @@ -64,6 +64,7 @@ export type ErrorConstructor = (HasParams< } : { new (): ErrorInstance; + new (opts: ErrorOpts): ErrorInstance; new (message: string): ErrorInstance; new (message: string | undefined, opts: ErrorOpts): ErrorInstance; }) & { diff --git a/index.js b/index.js index 4f6ddc5..84045d2 100644 --- a/index.js +++ b/index.js @@ -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; @@ -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) diff --git a/index.test-d.ts b/index.test-d.ts index e6f7905..3b3cb2e 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -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 // ────────────────────────────────────────────── diff --git a/test/index.test.js b/test/index.test.js index 3f6b97d..b83319a 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -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({