From 830b6441ff11e8194cb78f54a1cfd081caec0e39 Mon Sep 17 00:00:00 2001 From: Franco Pablo Romano Losada Date: Tue, 23 Dec 2025 10:58:45 +0000 Subject: [PATCH] Enhance safe() overloads --- packages/errata/src/client.ts | 25 ++++++++++++++++--------- packages/errata/src/errata.ts | 23 +++++++++++++++-------- packages/errata/test/client.test.ts | 9 +++++++++ packages/errata/test/errata.test.ts | 10 ++++++++++ 4 files changed, 50 insertions(+), 17 deletions(-) diff --git a/packages/errata/src/client.ts b/packages/errata/src/client.ts index abe1ce7..8707839 100644 --- a/packages/errata/src/client.ts +++ b/packages/errata/src/client.ts @@ -119,11 +119,14 @@ export interface ErrorClient { Extract, CodeOf> > /** Promise helper that returns a tuple without try/catch. */ - safe: ( - promise: Promise, - ) => Promise< - [data: T, error: null] | [data: null, error: ErrataClientErrorForCodes>] - > + safe: { + (fn: () => T | Promise): Promise< + [data: T, error: null] | [data: null, error: ErrataClientErrorForCodes>] + > + (promise: Promise): Promise< + [data: T, error: null] | [data: null, error: ErrataClientErrorForCodes>] + > + } } type InferCodes = T extends ErrataInstance @@ -327,12 +330,16 @@ export function createErrorClient>( return (err.tags ?? []).includes(tag) } - /** Promise helper returning a tuple without try/catch at call sites. */ - const safe = async ( - promise: Promise, + /** Promise/helper returning a tuple without try/catch at call sites. */ + const safe = (async ( + input: Promise | (() => T | Promise), ): Promise< [data: T, error: null] | [data: null, error: ErrataClientErrorForCodes] > => { + const promise = typeof input === 'function' + ? new Promise(resolve => resolve((input as () => T | Promise)())) + : input + try { const data = await promise return [data, null] @@ -340,7 +347,7 @@ export function createErrorClient>( catch (err) { return [null, ensure(err)] } - } + }) as ErrorClient['safe'] return { ErrataError: ErrataClientError, diff --git a/packages/errata/src/errata.ts b/packages/errata/src/errata.ts index 5bcd960..32876d2 100644 --- a/packages/errata/src/errata.ts +++ b/packages/errata/src/errata.ts @@ -121,11 +121,14 @@ export interface ErrataInstance { ): BoundaryErrataError | InternalCode> } /** Promise helper that returns a `[data, error]` tuple without try/catch. */ - safe: ( - promise: Promise, - ) => Promise< - [data: T, error: null] | [data: null, error: BoundaryErrataError | InternalCode>] - > + safe: { + (fn: () => T | Promise): Promise< + [data: T, error: null] | [data: null, error: BoundaryErrataError | InternalCode>] + > + (promise: Promise): Promise< + [data: T, error: null] | [data: null, error: BoundaryErrataError | InternalCode>] + > + } /** * Type-safe pattern check; supports exact codes, wildcard patterns (`'auth.*'`), * and arrays of patterns. Returns a type guard narrowing the error type. @@ -447,11 +450,15 @@ export function errata< const ensure = ensureFn as ErrataInstance['ensure'] /** Promise helper that returns a `[data, error]` tuple without try/catch. */ - const safe = async ( - promise: Promise, + const safe = (async ( + input: Promise | (() => T | Promise), ): Promise< [data: T, error: null] | [data: null, error: BoundaryErrataError] > => { + const promise = typeof input === 'function' + ? new Promise(resolve => resolve((input as () => T | Promise)())) + : input + try { const data = await promise return [data, null] @@ -459,7 +466,7 @@ export function errata< catch (err) { return [null, ensure(err)] } - } + }) as ErrataInstance['safe'] /** Type-safe pattern check; supports exact codes, wildcard patterns, and arrays. */ const is = (( diff --git a/packages/errata/test/client.test.ts b/packages/errata/test/client.test.ts index 342b0dc..119c95c 100644 --- a/packages/errata/test/client.test.ts +++ b/packages/errata/test/client.test.ts @@ -328,6 +328,15 @@ describe('client safe()', () => { expectTypeOf(err.code).toEqualTypeOf() } }) + + it('captures synchronous throws from function input', async () => { + const [data, err] = await client.safe(() => { + throw new Error('boom') + }) + + expect(data).toBeNull() + expect(err?.code).toBe('errata.unknown_error') + }) }) describe('client onUnknown hook', () => { diff --git a/packages/errata/test/errata.test.ts b/packages/errata/test/errata.test.ts index 35a8121..6d49afa 100644 --- a/packages/errata/test/errata.test.ts +++ b/packages/errata/test/errata.test.ts @@ -209,6 +209,16 @@ describe('errata basics', () => { expectTypeOf(value).toEqualTypeOf() } }) + + it('handles synchronous throw from function input', async () => { + const [value, err] = await errors.safe(() => { + throw new Error('boom') + }) + + expect(value).toBeNull() + expect(err).toBeInstanceOf(errors.ErrataError) + expect(err?.code).toBe('errata.unknown_error') + }) }) describe('onUnknown hook', () => {