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
34 changes: 20 additions & 14 deletions docs/internal/tasks/implementation/task-06-causes-function.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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

Expand All@@ -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

Expand Down
45 changes: 45 additions & 0 deletions packages/errors/src/causes/index.ts
Original file line numberDiff line numberDiff line change
@@ -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 };
3 changes: 3 additions & 0 deletions packages/errors/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
127 changes: 127 additions & 0 deletions packages/errors/tests/causes.test.ts
Original file line numberDiff line numberDiff line change
@@ -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 );
} );
} );
} );