From e0e0a1f41476b6ae1918599e90936ed8b12dd209 Mon Sep 17 00:00:00 2001 From: theBGuy <60308670+theBGuy@users.noreply.github.com> Date: Thu, 21 May 2026 20:39:30 +0000 Subject: [PATCH 1/2] fix(async-retryer): preserve message and cause for non-Error throws --- packages/pacer/src/async-retryer.ts | 12 +++++++++++- packages/pacer/tests/async-retryer.test.ts | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/pacer/src/async-retryer.ts b/packages/pacer/src/async-retryer.ts index 4bbce86d2..8e1f7f443 100644 --- a/packages/pacer/src/async-retryer.ts +++ b/packages/pacer/src/async-retryer.ts @@ -538,7 +538,17 @@ export class AsyncRetryer { ) { return undefined } - lastError = error instanceof Error ? error : new Error(String(error)) + lastError = + error instanceof Error + ? error + : new Error( + typeof error === 'object' && + error !== null && + typeof (error as any).message === 'string' + ? (error as any).message + : String(error), + { cause: error }, + ) this.#setState({ lastError }) // Call onError for every error (including during retries) diff --git a/packages/pacer/tests/async-retryer.test.ts b/packages/pacer/tests/async-retryer.test.ts index 448ea9194..1b75d8aa4 100644 --- a/packages/pacer/tests/async-retryer.test.ts +++ b/packages/pacer/tests/async-retryer.test.ts @@ -387,6 +387,24 @@ describe('AsyncRetryer', () => { expect(onLastError).toHaveBeenCalledTimes(1) expect(onLastError).toHaveBeenCalledWith(error, retryer) }) + + it('should preserve plain object error message and cause', async () => { + const originalError = { message: 'readable error', code: 'E001' } + const mockFn = vi.fn().mockRejectedValue(originalError) + const retryer = new AsyncRetryer(mockFn, { + maxAttempts: 1, + throwOnError: true, + }) + + try { + await retryer.execute() + expect.unreachable('Expected execute to throw') + } catch (error) { + expect(error).toBeInstanceOf(Error) + expect((error as Error).message).toBe('readable error') + expect((error as Error & { cause?: unknown }).cause).toBe(originalError) + } + }) }) describe('State Management', () => { From 4a83cc6429d10055f9141c76611409085f70b928 Mon Sep 17 00:00:00 2001 From: theBGuy <60308670+theBGuy@users.noreply.github.com> Date: Thu, 21 May 2026 20:43:41 +0000 Subject: [PATCH 2/2] chore: add changeset for async-retryer error normalization fix --- .changeset/big-bikes-wash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/big-bikes-wash.md diff --git a/.changeset/big-bikes-wash.md b/.changeset/big-bikes-wash.md new file mode 100644 index 000000000..cb824bf7d --- /dev/null +++ b/.changeset/big-bikes-wash.md @@ -0,0 +1,5 @@ +--- +'@tanstack/pacer': patch +--- + +Fix AsyncRetryer error normalization to preserve plain-object message values and attach the original thrown value as cause.