From e8e7c4042fdcf68373a845fa108111d3561982db Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 9 Mar 2026 19:58:58 +0000 Subject: [PATCH 1/4] Preserve error cause in getContractError --- src/errors/abi.ts | 12 ++++++++++-- src/errors/contract.ts | 15 ++++++++++++--- src/utils/abi/decodeErrorResult.ts | 8 +++++--- src/utils/errors/getContractError.ts | 3 ++- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/errors/abi.ts b/src/errors/abi.ts index d4a1571463..c108c3a8d9 100644 --- a/src/errors/abi.ts +++ b/src/errors/abi.ts @@ -100,9 +100,10 @@ export type AbiDecodingZeroDataErrorType = AbiDecodingZeroDataError & { name: 'AbiDecodingZeroDataError' } export class AbiDecodingZeroDataError extends BaseError { - constructor() { + constructor({ cause }: { cause?: BaseError | Error | undefined } = {}) { super('Cannot decode zero data ("0x") with ABI parameters.', { name: 'AbiDecodingZeroDataError', + cause, }) } } @@ -210,7 +211,13 @@ export type AbiErrorSignatureNotFoundErrorType = export class AbiErrorSignatureNotFoundError extends BaseError { signature: Hex - constructor(signature: Hex, { docsPath }: { docsPath: string }) { + constructor( + signature: Hex, + { + docsPath, + cause, + }: { docsPath: string; cause?: BaseError | Error | undefined }, + ) { super( [ `Encoded error signature "${signature}" not found on ABI.`, @@ -220,6 +227,7 @@ export class AbiErrorSignatureNotFoundError extends BaseError { { docsPath, name: 'AbiErrorSignatureNotFoundError', + cause, }, ) this.signature = signature diff --git a/src/errors/contract.ts b/src/errors/contract.ts index b85be5ec04..d03cf7d8dc 100644 --- a/src/errors/contract.ts +++ b/src/errors/contract.ts @@ -179,11 +179,13 @@ export class ContractFunctionRevertedError extends BaseError { data, functionName, message, + cause: error, }: { abi: Abi data?: Hex | undefined functionName: string message?: string | undefined + cause?: BaseError | Error | undefined }) { let cause: Error | undefined let decodedData: DecodeErrorResultReturnType | undefined @@ -191,7 +193,7 @@ export class ContractFunctionRevertedError extends BaseError { let reason: string | undefined if (data && data !== '0x') { try { - decodedData = decodeErrorResult({ abi, data }) + decodedData = decodeErrorResult({ abi, data, cause: error }) const { abiItem, errorName, args: errorArgs } = decodedData if (errorName === 'Error') { reason = (errorArgs as [string])[0] @@ -246,7 +248,7 @@ export class ContractFunctionRevertedError extends BaseError { ].join('\n') : `The contract function "${functionName}" reverted.`, { - cause, + cause: cause ?? error, metaMessages, name: 'ContractFunctionRevertedError', }, @@ -264,7 +266,13 @@ export type ContractFunctionZeroDataErrorType = name: 'ContractFunctionZeroDataError' } export class ContractFunctionZeroDataError extends BaseError { - constructor({ functionName }: { functionName: string }) { + constructor({ + functionName, + cause, + }: { + functionName: string + cause?: BaseError | Error | undefined + }) { super(`The contract function "${functionName}" returned no data ("0x").`, { metaMessages: [ 'This could be due to any of the following:', @@ -273,6 +281,7 @@ export class ContractFunctionZeroDataError extends BaseError { ' - The address is not a contract.', ], name: 'ContractFunctionZeroDataError', + cause, }) } } diff --git a/src/utils/abi/decodeErrorResult.ts b/src/utils/abi/decodeErrorResult.ts index 87672261b0..450abb3bb1 100644 --- a/src/utils/abi/decodeErrorResult.ts +++ b/src/utils/abi/decodeErrorResult.ts @@ -7,6 +7,7 @@ import { AbiErrorSignatureNotFoundError, type AbiErrorSignatureNotFoundErrorType, } from '../../errors/abi.js' +import type { BaseError } from '../../errors/base.js' import type { ErrorType } from '../../errors/utils.js' import type { AbiItem, @@ -28,7 +29,7 @@ import { type FormatAbiItemErrorType, formatAbiItem } from './formatAbiItem.js' export type DecodeErrorResultParameters< abi extends Abi | readonly unknown[] = Abi, -> = { abi?: abi | undefined; data: Hex } +> = { abi?: abi | undefined; data: Hex; cause?: BaseError | Error | undefined } export type DecodeErrorResultReturnType< abi extends Abi | readonly unknown[] = Abi, @@ -65,10 +66,10 @@ export type DecodeErrorResultErrorType = export function decodeErrorResult( parameters: DecodeErrorResultParameters, ): DecodeErrorResultReturnType { - const { abi, data } = parameters as DecodeErrorResultParameters + const { abi, data, cause } = parameters as DecodeErrorResultParameters const signature = slice(data, 0, 4) - if (signature === '0x') throw new AbiDecodingZeroDataError() + if (signature === '0x') throw new AbiDecodingZeroDataError({ cause }) const abi_ = [...(abi || []), solidityError, solidityPanic] const abiItem = abi_.find( @@ -78,6 +79,7 @@ export function decodeErrorResult( if (!abiItem) throw new AbiErrorSignatureNotFoundError(signature, { docsPath: '/docs/contract/decodeErrorResult', + cause, }) return { abiItem, diff --git a/src/utils/errors/getContractError.ts b/src/utils/errors/getContractError.ts index 849e57751c..e11d674330 100644 --- a/src/utils/errors/getContractError.ts +++ b/src/utils/errors/getContractError.ts @@ -57,7 +57,7 @@ export function getContractError>( const cause = (() => { if (err instanceof AbiDecodingZeroDataError) - return new ContractFunctionZeroDataError({ functionName }) + return new ContractFunctionZeroDataError({ functionName, cause: err }) if ( ([EXECUTION_REVERTED_ERROR_CODE, InternalRpcError.code].includes(code) && (data || details || message || shortMessage)) || @@ -73,6 +73,7 @@ export function getContractError>( error instanceof RpcRequestError ? details : (shortMessage ?? message), + cause: err, }) } return err From d3eff66f630da0e7cc1556f4c0b05a0a6f7ef6d8 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 9 Mar 2026 20:42:17 +0000 Subject: [PATCH 2/4] Update inline snapshots of errors that now have `details` --- src/utils/errors/getContractError.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/utils/errors/getContractError.test.ts b/src/utils/errors/getContractError.test.ts index e1a9aa86eb..0dddc05ad9 100644 --- a/src/utils/errors/getContractError.test.ts +++ b/src/utils/errors/getContractError.test.ts @@ -67,12 +67,14 @@ describe('getContractError', () => { args: (1) sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + Details: execution reverted: Sale must be active to mint Ape Version: viem@x.y.z] `) expect(error.cause).toMatchInlineSnapshot(` [ContractFunctionRevertedError: The contract function "mintApe" reverted with the following reason: Sale must be active to mint Ape + Details: execution reverted: Sale must be active to mint Ape Version: viem@x.y.z] `) }) @@ -102,12 +104,14 @@ describe('getContractError', () => { args: (1) sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + Details: execution reverted: Sale must be active to mint Ape Version: viem@x.y.z] `) expect(error.cause).toMatchInlineSnapshot(` [ContractFunctionRevertedError: The contract function "mintApe" reverted with the following reason: Sale must be active to mint Ape + Details: execution reverted: Sale must be active to mint Ape Version: viem@x.y.z] `) }) @@ -171,12 +175,14 @@ describe('getContractError', () => { args: (1) sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + Details: ah no Version: viem@x.y.z] `) expect(error.cause).toMatchInlineSnapshot(` [ContractFunctionRevertedError: The contract function "mintApe" reverted with the following reason: ah no + Details: ah no Version: viem@x.y.z] `) }) @@ -240,12 +246,14 @@ describe('getContractError', () => { args: (1) sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + Details: ah no Version: viem@x.y.z] `) expect(error.cause).toMatchInlineSnapshot(` [ContractFunctionRevertedError: The contract function "mintApe" reverted with the following reason: ah no + Details: ah no Version: viem@x.y.z] `) }) @@ -275,12 +283,14 @@ describe('getContractError', () => { args: (1) sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + Details: execution reverted: Sale must be active to mint Ape Version: viem@x.y.z] `) expect(error.cause).toMatchInlineSnapshot(` [ContractFunctionRevertedError: The contract function "mintApe" reverted with the following reason: Sale must be active to mint Ape + Details: execution reverted: Sale must be active to mint Ape Version: viem@x.y.z] `) }) From d6ddd41d95b4c94f5262efefc4256b5c3b6fe685 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 9 Mar 2026 20:42:30 +0000 Subject: [PATCH 3/4] Add tests verifying that the cause is preserved --- src/utils/errors/getContractError.test.ts | 95 ++++++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/src/utils/errors/getContractError.test.ts b/src/utils/errors/getContractError.test.ts index 0dddc05ad9..f30bc6fc9b 100644 --- a/src/utils/errors/getContractError.test.ts +++ b/src/utils/errors/getContractError.test.ts @@ -2,9 +2,17 @@ import { describe, expect, test } from 'vitest' import { baycContractConfig } from '~test/abis.js' import { accounts } from '~test/constants.js' -import { AbiDecodingZeroDataError } from '../../errors/abi.js' +import { + AbiDecodingZeroDataError, + AbiErrorSignatureNotFoundError, +} from '../../errors/abi.js' import { BaseError } from '../../errors/base.js' -import { RawContractError } from '../../errors/contract.js' +import { + ContractFunctionExecutionError, + ContractFunctionRevertedError, + ContractFunctionZeroDataError, + RawContractError, +} from '../../errors/contract.js' import { RpcRequestError } from '../../errors/request.js' import { getContractError } from './getContractError.js' @@ -370,6 +378,89 @@ describe('getContractError', () => { `) }) + describe('preserves error cause', () => { + test('preserves the cause when receiving an AbiDecodingZeroDataError', () => { + const originalError = new AbiDecodingZeroDataError() + const error = getContractError(originalError, { + abi: baycContractConfig.abi, + functionName: 'mintApe', + args: [1n], + sender: accounts[0].address, + }) + expect(error).toBeInstanceOf(ContractFunctionExecutionError) + expect(error.cause).toBeInstanceOf(ContractFunctionZeroDataError) + expect(error.cause.cause).toBe(originalError) + }) + + describe('when wrapping it in a ContractFunctionRevertedError error', () => { + test('preserves the cause when the return data decoding succeeds', () => { + const originalError = new RawContractError({ + message: 'execution reverted: Sale must be active to mint Ape', + data: '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001f53616c65206d7573742062652061637469766520746f206d696e742041706500', + }) + const error = getContractError(originalError, { + abi: baycContractConfig.abi, + functionName: 'mintApe', + args: [1n], + sender: accounts[0].address, + }) + expect(error).toBeInstanceOf(ContractFunctionExecutionError) + expect(error.cause).toBeInstanceOf(ContractFunctionRevertedError) + expect(error.cause.cause).toBe(originalError) + }) + + test('preserves the cause when decoding the return data throws an AbiErrorSignatureNotFoundError error', () => { + // Data with unknown error signature (not in ABI) + padding + const originalError = new RawContractError({ + message: 'execution reverted', + data: '0xdeadbeef0000000000000000000000000000000000000000000000000000000000000000', + }) + const error = getContractError(originalError, { + abi: baycContractConfig.abi, + functionName: 'mintApe', + args: [1n], + sender: accounts[0].address, + }) + expect(error).toBeInstanceOf(ContractFunctionExecutionError) + expect(error.cause).toBeInstanceOf(ContractFunctionRevertedError) + expect(error.cause.cause).toBeInstanceOf(AbiErrorSignatureNotFoundError) + expect( + (error.cause.cause as AbiErrorSignatureNotFoundError).cause, + ).toBe(originalError) + }) + + test('looses the cause when decoding the return data throws an other error (known limitation)', () => { + // Valid Error(string) selector but malformed/truncated params so decodeAbiParameters throws + const originalError = new RawContractError({ + message: 'execution reverted', + data: '0x08c379a0000000000000000000000000000000000000000000000000000000000000ffff', + }) + const error = getContractError(originalError, { + abi: baycContractConfig.abi, + functionName: 'mintApe', + args: [1n], + sender: accounts[0].address, + }) + expect(error).toBeInstanceOf(ContractFunctionExecutionError) + expect(error.cause).toBeInstanceOf(ContractFunctionRevertedError) + // The cause is the decode error, not the original error — known limitation + expect(error.cause.cause).not.toBe(originalError) + }) + }) + + test('preserves the cause as the direct error.cause on every other case', () => { + const originalError = new BaseError('some unknown error') + const error = getContractError(originalError, { + abi: baycContractConfig.abi, + functionName: 'mintApe', + args: [1n], + sender: accounts[0].address, + }) + expect(error).toBeInstanceOf(ContractFunctionExecutionError) + expect(error.cause).toBe(originalError) + }) + }) + test('zero data', () => { const error = getContractError(new AbiDecodingZeroDataError(), { abi: baycContractConfig.abi, From 2cd51528f15c2182b89cc97fe53ba60f9fe3e198 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:15:34 +1300 Subject: [PATCH 4/4] Create six-pumpkins-wait.md --- .changeset/six-pumpkins-wait.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/six-pumpkins-wait.md diff --git a/.changeset/six-pumpkins-wait.md b/.changeset/six-pumpkins-wait.md new file mode 100644 index 0000000000..7740874803 --- /dev/null +++ b/.changeset/six-pumpkins-wait.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Added error preservation in `getContractError`.