From 20e3fda19041e37617928f3c8ae442ce912c62fc Mon Sep 17 00:00:00 2001 From: martyy-code Date: Fri, 29 May 2026 11:11:33 +0200 Subject: [PATCH 1/2] feat: implement causes() function for cause chain traversal Add causes(error) function that returns the causes array from an error: - Returns array from most recent to root cause - Handles errors with no cause (returns empty array) - Works through multiple chaining levels - Works with native errors in chain The causes property (err.causes) already works as per Task 05. Co-Authored-By: Claude Opus 4.7 --- packages/errors/src/causes/index.ts | 45 ++++++++++ packages/errors/src/index.ts | 3 + packages/errors/tests/causes.test.ts | 127 +++++++++++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 packages/errors/src/causes/index.ts create mode 100644 packages/errors/tests/causes.test.ts diff --git a/packages/errors/src/causes/index.ts b/packages/errors/src/causes/index.ts new file mode 100644 index 0000000..693c126 --- /dev/null +++ b/packages/errors/src/causes/index.ts @@ -0,0 +1,45 @@ +/** + * Cause chain traversal utilities. + */ + +import type { ErrorInstance } from '../error/types.js'; + +/** + * Returns all causes in the error chain, from most recent to root cause. + * + * @param error - The error to get causes from + * @returns Array of errors in the cause chain, ordered newest to oldest + * + * @example + * ```typescript try { + * // ... } catch (err) { const chain = causes(err); + * chain.forEach(e => logError(e)); + * } + * ``` + * + * @example + * ```typescript + * const err = ValidationError({ field: 'email' }) + * .from(new NetworkError('Connection failed')) + * .from(new Error('DNS lookup failed')); + * + * // causes(err) returns newest-to-oldest: [NetworkError, Error] + * // (err.cause is NetworkError, err.cause.cause is Error) + * ``` + */ +const causes = ( error: unknown ): Error[] => { + if ( error == null ) { + return []; + } + + // Get the causes array from the error + const instance = error as ErrorInstance; + + if ( Array.isArray( instance.causes ) ) { + return instance.causes; + } + + return []; +}; + +export { causes }; diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts index caa8939..e4a0ed5 100644 --- a/packages/errors/src/index.ts +++ b/packages/errors/src/index.ts @@ -20,3 +20,6 @@ export { raise } from './raise/index.js'; // Error type checking function export { is } from './is/index.js'; + +// Cause chain traversal function +export { causes } from './causes/index.js'; diff --git a/packages/errors/tests/causes.test.ts b/packages/errors/tests/causes.test.ts new file mode 100644 index 0000000..7e4469a --- /dev/null +++ b/packages/errors/tests/causes.test.ts @@ -0,0 +1,127 @@ +/** + * Unit tests for the causes() function. + */ + +import { describe, it, expect } from 'vitest'; +import { error, causes } from '../src/index.js'; + +describe( 'causes() function', () => { + describe( 'basic usage', () => { + it( 'should return causes array from error instance', () => { + const AppError = error( { name: 'AppError' } ); + const ValidationError = error( { name: 'ValidationError' } ); + + const cause = AppError(); + const instance = ValidationError(); + instance.from( cause ); + + const result = causes( instance ); + + expect( result ).toHaveLength( 1 ); + expect( result[0] ).toBe( cause ); + } ); + + it( 'should return empty array for error with no cause', () => { + const AppError = error( { name: 'AppError' } ); + const instance = AppError(); + + const result = causes( instance ); + + expect( result ).toEqual( [] ); + } ); + } ); + + describe( 'ordering', () => { + it( 'should return array ordered from most recent to root cause', () => { + const AppError = error( { name: 'AppError' } ); + const cause1 = AppError(); + const cause2 = AppError(); + const cause3 = AppError(); + cause3.from( cause2 ).from( cause1 ); + + const instance = AppError(); + instance.from( cause3 ); + + const result = causes( instance ); + + // result contains cause3, cause2, cause1 (newest to oldest) + expect( result ).toContain( cause3 ); + expect( result ).toContain( cause2 ); + expect( result ).toContain( cause1 ); + } ); + + it( 'should work with single level chaining', () => { + const AppError = error( { name: 'AppError' } ); + const cause = AppError(); + const instance = AppError(); + instance.from( cause ); + + const result = causes( instance ); + + expect( result ).toHaveLength( 1 ); + expect( result[0] ).toBe( cause ); + } ); + + it( 'should work with multiple level chaining', () => { + const AppError = error( { name: 'AppError' } ); + const err1 = AppError(); + const err2 = AppError(); + const err3 = AppError(); + + err2.from( err1 ); + err3.from( err2 ); + + const result = causes( err3 ); + + expect( result ).toHaveLength( 2 ); + expect( result[0] ).toBe( err2 ); + expect( result[1] ).toBe( err1 ); + } ); + } ); + + describe( 'native errors', () => { + it( 'should handle native errors in chain', () => { + const AppError = error( { name: 'AppError' } ); + const instance = AppError(); + instance.from( new Error( 'native cause' ) ); + + const result = causes( instance ); + + expect( result ).toHaveLength( 1 ); + expect( result[0] ).toBeInstanceOf( Error ); + expect( result[0].message ).toBe( 'native cause' ); + } ); + + it( 'should return empty array for native error without causes', () => { + const result = causes( new Error( 'test' ) ); + + expect( result ).toEqual( [] ); + } ); + } ); + + describe( 'edge cases', () => { + it( 'should return empty array for null', () => { + expect( causes( null ) ).toEqual( [] ); + } ); + + it( 'should return empty array for undefined', () => { + expect( causes( undefined ) ).toEqual( [] ); + } ); + + it( 'should return empty array for non-error values', () => { + expect( causes( 'string' ) ).toEqual( [] ); + expect( causes( 123 ) ).toEqual( [] ); + expect( causes( {} ) ).toEqual( [] ); + } ); + + it( 'should return causes property directly', () => { + const AppError = error( { name: 'AppError' } ); + const cause = AppError(); + const instance = AppError(); + instance.from( cause ); + + // The causes() function returns the same as err.causes property + expect( causes( instance ) ).toBe( instance.causes ); + } ); + } ); +} ); From fa6f4e38bec7d60cc508585fb2938c0250e556c0 Mon Sep 17 00:00:00 2001 From: martyy-code Date: Fri, 29 May 2026 11:12:10 +0200 Subject: [PATCH 2/2] docs: mark task-06 as complete causes() function implemented and tested. All acceptance criteria met. Co-Authored-By: Claude Opus 4.7 --- .../implementation/task-06-causes-function.md | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/docs/internal/tasks/implementation/task-06-causes-function.md b/docs/internal/tasks/implementation/task-06-causes-function.md index f92c58c..821a194 100644 --- a/docs/internal/tasks/implementation/task-06-causes-function.md +++ b/docs/internal/tasks/implementation/task-06-causes-function.md @@ -2,18 +2,24 @@ ## Status -🟡 Pending +✅ Complete ## Description Implement causes(err) function that returns array of all errors in the cause chain. +## Implementation + +Implemented in: +- `src/causes/index.ts` - causes() function +- `src/index.ts` - exported for public API + ## Requirements -- Return array from most recent to root cause -- Handle errors with no cause -- Handle native errors in chain -- Provide both function and property access +- [x] Return array from most recent to root cause +- [x] Handle errors with no cause +- [x] Handle native errors in chain +- [x] Provide both function and property access ## API @@ -26,21 +32,21 @@ err.causes; // ErrorInstance[] ## Acceptance Criteria -- [ ] Returns array ordered most recent first -- [ ] Returns empty array for error with no cause -- [ ] Works through multiple chaining levels -- [ ] Handles native errors in chain -- [ ] `err.causes` property works same as causes(err) +- [x] Returns array ordered most recent first +- [x] Returns empty array for error with no cause +- [x] Works through multiple chaining levels +- [x] Handles native errors in chain +- [x] `err.causes` property works same as causes(err) ## Dependencies -- Task 01: error() factory -- Task 05: .from() method +- Task 01: error() factory ✅ +- Task 05: .from() method ✅ ## Related Tasks -- Task 13: Unit tests for causes() -- Task 05: .from() method +- Task 13: Unit tests for causes() ✅ +- Task 05: .from() method ✅ ## Notes