diff --git a/source/index.ts b/source/index.ts index a89e794..de07278 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1,6 +1,7 @@ export * from './event-emitter.ts'; export * from './exponential-backoff.ts'; export * from './extended-json.ts'; +export * from './misc.ts'; export * from './script.ts'; export * from './sse-session/index.ts'; export * from './template/errors.ts'; diff --git a/source/misc.ts b/source/misc.ts new file mode 100644 index 0000000..eae25c9 --- /dev/null +++ b/source/misc.ts @@ -0,0 +1,15 @@ +/** + * Tries to execute an async function and handles any errors that occur. + * @param fn - The function to execute. + * @param onError - The callback to call if the function fails. + * @returns The result of the function. + */ +export const tryAsync = async (fn: () => unknown, onError?: (error: Error) => void): Promise => { + try { + await fn(); + } catch (error) { + const errorInstance = error instanceof Error ? error : new Error(`${error}`); + + onError?.(errorInstance); + } +}; diff --git a/test/misc.test.ts b/test/misc.test.ts new file mode 100644 index 0000000..20b6bce --- /dev/null +++ b/test/misc.test.ts @@ -0,0 +1,70 @@ +import { expect, test, vi } from 'vitest'; +import { tryAsync } from '../source/misc.ts'; + +/** Spy used to confirm the wrapped async function ran successfully. */ +const successFlagFn = vi.fn(); + +/** Spy used to confirm the error callback was invoked on failure. */ +const errorFlagFn = vi.fn(); + +/** + * Tests that tryAsync invokes the function and skips the error callback on success. + */ +const testTryAsyncCallsFunctionOnSuccess = async (): Promise => { + // Reset spies so prior test runs do not affect call counts. + vi.clearAllMocks(); + + const successFn = async (): Promise => { + successFlagFn(); + }; + + await tryAsync(successFn); + + // The wrapped function should run and no error handler should be called. + expect(successFlagFn).toHaveBeenCalledOnce(); + expect(errorFlagFn).not.toHaveBeenCalled(); +}; + +/** + * Tests that tryAsync invokes the error callback when the function throws. + */ +const testTryAsyncCallsErrorCallbackOnFailure = async (): Promise => { + vi.clearAllMocks(); + + const errorFn = async (): Promise => { + throw new Error('test'); + }; + + await tryAsync(errorFn, errorFlagFn); + + // The success path should not run; the error callback should receive the failure. + expect(successFlagFn).not.toHaveBeenCalled(); + expect(errorFlagFn).toHaveBeenCalledOnce(); +}; + +/** + * Tests that tryAsync wraps non-Error throws in Error instances before calling the error callback. + */ +const testTryAsyncConvertsNonErrorThrows = async (): Promise => { + vi.clearAllMocks(); + + const errorFn = async (): Promise => { + /* eslint-disable-next-line */ + throw 'test'; + }; + + await tryAsync(errorFn, errorFlagFn); + + // Non-Error throws must be normalized to Error before onError is called. + expect(successFlagFn).not.toHaveBeenCalled(); + expect(errorFlagFn).toHaveBeenCalledOnce(); + expect(errorFlagFn).toHaveBeenCalledWith(new Error('test')); +}; + +const runTests = async (): Promise => { + test('tryAsync: calls the function and skips the error callback on success', testTryAsyncCallsFunctionOnSuccess); + test('tryAsync: calls the error callback when the function fails', testTryAsyncCallsErrorCallbackOnFailure); + test('tryAsync: converts non-Error throws to Error instances', testTryAsyncConvertsNonErrorThrows); +}; + +await runTests();