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
48 changes: 35 additions & 13 deletions packages/errors/src/error/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ import type { ErrorFactory, ErrorInstance } from './types.js';
import { captureStack } from './capture.js';
import { formatTemplate, hasTemplatePlaceholders } from './format.js';

// ============================================================================
// Symbols for identity
// ============================================================================

/**
* Symbol used to identify factory-created errors.
* Stored on the error instance to enable reliable instanceof checks.
*
* @internal
*/
const FACTORY_SYMBOL = Symbol.for( '@deessejs/errors/factory' );

// ============================================================================
// Error Factory
// ============================================================================
Expand Down Expand Up @@ -85,21 +97,25 @@ export const error = <const T extends Record<string, unknown> = Record<string, n
errorMessage = message;
}

// Capture stack trace with cleaned frames
// Capture stack trace
const stack = captureStack( errorMessage );

return {
name,
message: errorMessage,
stack,
fields: fieldsData,
notes: [],
cause: null,
causes: [],
context: null,
inherits: inherits ?? undefined,
_factory: ErrorFactoryInstance as ErrorFactory<T>,
};
// Create error instance using native Error
const instance = new Error( errorMessage ) as ErrorInstance<T>;
instance.name = name;
instance.fields = fieldsData;
instance.notes = [];
instance.cause = null;
instance.causes = [];
instance.context = null;
instance.inherits = inherits ?? undefined;
instance.stack = stack;

// Mark this instance as created by this factory (for is() checks)
// Use callable to avoid generic parameter conflicts
( instance as unknown as Record<typeof FACTORY_SYMBOL, () => unknown> )[FACTORY_SYMBOL] = ErrorFactoryInstance;

return instance;
};

// Attach metadata to the factory function
Expand All @@ -124,3 +140,9 @@ export const error = <const T extends Record<string, unknown> = Record<string, n

return ErrorFactoryInstance as ErrorFactory<T>;
};

// ============================================================================
// Exports for is() function
// ============================================================================

export { FACTORY_SYMBOL };
2 changes: 0 additions & 2 deletions packages/errors/src/error/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ export type ErrorInstance<TFields extends Record<string, unknown> = Record<strin
context: Record<string, unknown> | null;
/** Parent error factories for type checking */
inherits?: ErrorFactory | ErrorFactory[];
/** Reference to the factory that created this instance */
_factory: ErrorFactory<TFields>;
};

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/errors/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ export { error } from './error/error.js';

// Error raising function
export { raise } from './raise/index.js';

// Error type checking function
export { is } from './is/index.js';
117 changes: 117 additions & 0 deletions packages/errors/src/is/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Error type checking utilities.
*/

import type { ErrorFactory, ErrorInstance } from '../error/types.js';
import { FACTORY_SYMBOL } from '../error/error.js';

/**
* Type to extract the fields from an ErrorFactory or native Error class.
*
* @internal
*/
type ExtractFields<T> = T extends ErrorFactory<infer F>
? F
: T extends new ( ...args: unknown[] ) => infer E
? E extends ErrorInstance<infer F>
? F
: Record<string, unknown>
: Record<string, unknown>;

/**
* Checks if an error is an instance of a specific error type.
*
* Works with:
* - Custom error factories created by error()
* - Single and multiple inheritance hierarchies
* - Native JavaScript errors (TypeError, SyntaxError, etc.)
*
* @param error - The error to check (can be any value)
* @param ErrorType - The error type to check against
* @returns boolean - true if the error is the specified type or inherits from it
*
* @example
* ```typescript
* const AppError = error({ name: 'AppError' });
* const ValidationError = error({ name: 'ValidationError', inherits: AppError });
*
* const err = ValidationError();
* is(err, ValidationError); // true
* is(err, AppError); // true (through inheritance)
* ```
*
* @example
* ```typescript
* // Works with native errors
* try {
* JSON.parse('invalid');
* } catch (err) {
* if (is(err, SyntaxError)) {
* // Handle syntax errors
* }
* }
* ```
*/
const is = <T extends ErrorFactory | ( new ( ...args: unknown[] ) => Error )>(
error: unknown,
ErrorType: T
): error is ErrorInstance<ExtractFields<T>> => {
// Handle null/undefined
if ( error == null ) {
return false;
}

// Handle native errors - check prototype chain ends in Error
if ( typeof ErrorType === 'function' && 'prototype' in ErrorType ) {
try {
if ( error instanceof ErrorType ) {
return true;
}
} catch {
// instanceof can fail for cross-realm errors
}
}

// Handle our ErrorFactory instances using Symbol-based reference
if ( typeof error === 'object' && error !== null ) {
const marker = error as Record<typeof FACTORY_SYMBOL, unknown>;
const factory = marker[FACTORY_SYMBOL];

if ( factory !== undefined ) {
// DFS walk of inheritance tree using stack (prevents GC pressure)
const stack: ErrorFactory[] = [factory as ErrorFactory];
const seen = new Set<ErrorFactory>();

while ( stack.length > 0 ) {
const current = stack.pop()!;

// Prevent infinite loops in cyclic inheritance
if ( seen.has( current ) ) {
continue;
}
seen.add( current );

// Direct match
if ( current === ErrorType ) {
return true;
}

// Add parents to stack
const inherits = ( current as ErrorFactory ).inherits;
if ( inherits !== undefined ) {
if ( Array.isArray( inherits ) ) {
for ( let i = 0; i < inherits.length; i++ ) {
stack.push( inherits[i] );
}
} else {
stack.push( inherits );
}
}
}
}
}

return false;
};

export { is };
11 changes: 5 additions & 6 deletions packages/errors/tests/error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ describe( 'error() factory function', () => {
expect( instance.context ).toBeNull();
} );

it( 'should have _factory reference back to the creator', () => {
it( 'should be an instance of Error', () => {
const TestError = error( { name: 'TestError' } );
const instance = TestError();

expect( instance._factory ).toBe( TestError );
expect( instance instanceof Error ).toBe( true );
} );

it( 'should have inherits reference when inheriting', () => {
Expand Down Expand Up @@ -304,14 +304,13 @@ describe( 'error() factory function', () => {
expect( ErrorA.name ).not.toBe( ErrorB.name );
} );

it( 'should maintain factory reference on instances', () => {
it( 'should create errors that are instances of Error', () => {
const TestError = error( { name: 'TestError' } );
const instance1 = TestError();
const instance2 = TestError();

expect( instance1._factory ).toBe( TestError );
expect( instance2._factory ).toBe( TestError );
expect( instance1._factory ).toBe( instance2._factory );
expect( instance1 instanceof Error ).toBe( true );
expect( instance2 instanceof Error ).toBe( true );
} );
} );

Expand Down
Loading