diff --git a/.changeset/add-addnote-method.md b/.changeset/add-addnote-method.md new file mode 100644 index 0000000..3bfbab7 --- /dev/null +++ b/.changeset/add-addnote-method.md @@ -0,0 +1,5 @@ +--- +"@deessejs/errors": minor +--- + +Add `ErrorInstance.addNote(note)` for attaching runtime context to errors (PEP 678, mirrors Python 3.11). Returns the instance for chaining. The `notes: string[]` property was already implemented; the method was missing despite being documented. Closes #29. diff --git a/packages/errors/src/error/error.ts b/packages/errors/src/error/error.ts index 45abf37..9f420a8 100644 --- a/packages/errors/src/error/error.ts +++ b/packages/errors/src/error/error.ts @@ -119,6 +119,12 @@ export const error = = Record => { + instance.notes.push(note); + return instance; + }; + // Mark this instance as created by this factory (for is() checks) // Use callable to avoid generic parameter conflicts (instance as unknown as Record unknown>)[FACTORY_SYMBOL] = diff --git a/packages/errors/src/error/types.ts b/packages/errors/src/error/types.ts index d024dd0..41ab0f6 100644 --- a/packages/errors/src/error/types.ts +++ b/packages/errors/src/error/types.ts @@ -36,16 +36,29 @@ export type ErrorFactory = Record = Record> = ErrorInstanceCore & { /** User-defined fields from Standard Schema */ fields: TFields; - // TODO: Implement .addNote() method (Task XX) /** Additional notes added via .addNote() */ notes: string[]; + /** + * Adds a note to this error instance. + * + * Notes provide runtime context that complements the structured fields. + * Patterned after Python 3.11's `BaseException.add_note()` (PEP 678). + * + * @param note - The note text to attach + * @returns This error instance for chaining + * + * @example + * ```typescript + * const err = AppError().addNote('Attempt 1 failed').addNote('Retrying...'); + * // err.notes === ['Attempt 1 failed', 'Retrying...'] + * ``` + */ + addNote(note: string): ErrorInstance; /** * Chains a cause error to this error. * diff --git a/packages/errors/tests/error.test.ts b/packages/errors/tests/error.test.ts index 864c921..c885ea5 100644 --- a/packages/errors/tests/error.test.ts +++ b/packages/errors/tests/error.test.ts @@ -86,6 +86,46 @@ describe('error() factory function', () => { }); }); + describe('.addNote()', () => { + it('should attach a single note to a fresh instance', () => { + const AppError = error({ name: 'AppError' }); + const instance = AppError(); + + const returned = instance.addNote('first attempt failed'); + + expect(returned).toBe(instance); + expect(instance.notes).toEqual(['first attempt failed']); + }); + + it('should append multiple notes in order', () => { + const AppError = error({ name: 'AppError' }); + const instance = AppError() + .addNote('Attempt 1 failed') + .addNote('Retrying...') + .addNote('Attempt 2 failed'); + + expect(instance.notes).toEqual(['Attempt 1 failed', 'Retrying...', 'Attempt 2 failed']); + }); + + it('should preserve notes through .from() chaining', () => { + const AppError = error({ name: 'AppError' }); + const cause = new Error('underlying failure'); + const instance = AppError().addNote('context A').from(cause).addNote('context B'); + + expect(instance.notes).toEqual(['context A', 'context B']); + expect(instance.cause).toBe(cause); + }); + + it('should isolate notes between sibling instances', () => { + const AppError = error({ name: 'AppError' }); + const a = AppError().addNote('only on a'); + const b = AppError(); + + expect(a.notes).toEqual(['only on a']); + expect(b.notes).toEqual([]); + }); + }); + describe('inherits option', () => { it('should support single inheritance', () => { const AppError = error({ name: 'AppError' });