You can handle custom errors from your Solidity smart contract in your application using ethers-decode-error quite easily. This utility also allows you to extract and use the parameters from custom errors if needed.
Consider a scenario where you have a contract method called swap
, and you want to catch a specific custom error called InvalidSwapToken
. You can do this using the following code:
import { ErrorDecoder } from 'ethers-decode-error'
const errorDecoder = ErrorDecoder.create([abi])
const MyCustomErrorContract = new ethers.Contract('0x12345678', abi, provider)
try {
const tx = await MyCustomErrorContract.swap('0xabcd', 123)
await tx.wait()
} catch (err) {
const decodedError = await errorDecoder.decode(err)
const reason = customReasonMapper(decodedError)
// Prints "Invalid swap with token contract address 0xabcd."
console.log('Custom error reason:', reason)
}
const customReasonMapper = ({ name, args }: DecodedError): string => {
switch (name) {
case 'InvalidSwapToken':
// You can access the error parameters using their index:
return `Invalid swap with token contract address ${args[0]}.`
// Or, you could also access the error parameters using their names:
return `Invalid swap with token contract address ${args['token']}.`
default:
return 'The transaction has reverted.'
}
}
The example above threw an InvalidSwapToken
custom error which has a parameter of the token address. The value of this parameter can be accessed from the args
variable when handling the error. If your customer error doesn't have any parameters, you can ignore the args
variable.
You can refer to the docs for more examples.
Here's how the ABI of the InvalidSwapToken
custom error with an input parameter might have looked like:
const abi = [
{
inputs: [
{
internalType: 'address',
name: 'token',
type: 'address',
},
],
name: 'InvalidSwapToken',
type: 'error',
},
]
It will be much easier if you're using Typechain as you can pass the contract interface as ABI:
const errorDecoder = ErrorDecoder.create([MyCustomErrorContract.interface])
Best Answer
Check out this function I modified from the openzepplin's Address.sol.
returndata
is a dynamic-sized byte array. If the delegate call fails,returndata
would be the error object raised by the failed call. Say I have contract A which delegate calls to contract B. The latter reverts with the custom errorGonnaMakeIt(uint256 data)
. Then, thereturndata
would be:Then, we make use of Yul's
revert(p, s)
to revert with the custom error in contract A.p
refers to the pointer of where the error byte array starts,s
refers to the how long the byte array is.Since
returndata
is a dynamic-sized byte array, the first 32 bit of the pointer stores the size of it, which wemload(returndata)
to retrieve it. And similarly,add(32, returndata)
would be the pointer to where the byte array starts. Altogther, it forms the linerevert(add(32, returndata), mload(returndata))
, which reverts the error you get from contract B.