ethers-decode-error
For those who've grappled with extracting the actual error message or reason from the JSON RPC when a transaction fails
or a smart contract reverts, you'll certainly appreciate how cumbersome it could at times.
This is a simple utility library to help simplify the process of determining the actual errors from smart contract. You simply pass in the error object and you will get the actual error message and a bunch of other information about the error. It works with the regular revert errors, panic errors, Metamask rejection error and custom
errors.
Installation
npm install ethers-decode-error --save
You will need to install ethers.js in your project if you have not:
npm install ethers@^6 --save
💡 If you wish to use it with ethers v5 instead, please refer to the v1 release.
Usage
This library decodes an ethers error object reverted from a smart contract into results that lets you decide the best course of action from there.
Start by creating an instance of the ErrorDecoder
:
import { ErrorDecoder } from 'ethers-decode-error'
const errorDecoder = ErrorDecoder.create()
The create
method optionally accepts an array of ABI or contract interface objects as its first argument. Although the ABI is not required for normal reverts, it's recommended to supply the ABI or contract interfaces if you're expecting custom errors. See the examples in Custom Errors section for more details.
After creating the instance, you can repeatedly use the decode
method throughout your code to decode error objects:
try {
} catch (err) {
const decodedError: decodedError = await errorDecoder.decode(err)
console.log(`Revert reason: ${decodedError.reason}`)
}
Decoded Error
The DecodedError
object is the result of the decoded error, which contains the following properties for handling the error occurred:
Property | Value Type | Remarks |
---|
type | ErrorType | The type of the error. See Error Types. |
reason | string | null | The decoded error message, or null if error is unknown or has no message. |
data | string | null | The raw data bytes returned from the contract error, or null if error is unknown or empty. |
args | Array | The parameter values of the error if exists. For custom errors, the args will always be empty if no ABI or interface is supplied for decoding. |
name | string | null | The name of the error. This can be used to identify the custom error emitted. If no ABI is supplied for custom error, this will be the selector hex. If error is RpcError , this will be the error code. null if error is EmptyError . |
selector | string | null | The hexidecimal value of the selector. null if error is EmptyError . |
signature | string | null | The signature of the error. null if error is EmptyError or no specified ABI for custom error. |
fragment | string | null | The ABI fragment of the error. null if error is EmptyError or no specified ABI for custom error. |
Error Types
These are the possible ErrorType
that could be returned for the type
property in the DecodedError
object:
Type | Description |
---|
ErrorType.EmptyError | Contract reverted without reason provided |
ErrorType.RevertError | Contract reverted with reason provided |
ErrorType.PanicError | Contract reverted due to a panic error |
ErrorType.CustomError | Contract reverted due to a custom error |
ErrorType.UserRejectError | User rejected the transaction |
ErrorType.RpcError | An error from the JSON RPC |
ErrorType.UnknownError | An unknown error was thrown |
Examples
Revert/Require Errors
import { ErrorDecoder } from 'ethers-decode-error'
const errorDecoder = ErrorDecoder.create()
const WETH = new ethers.Contract('0xC02aa...756Cc2', abi, provider)
try {
const tx = await WETH.transfer('0x0', amount)
await tx.wait()
} catch (err) {
const { reason } = await errorDecoder.decode(err)
console.log('Revert reason:', reason)
}
Panic Errors
import { ErrorDecoder } from 'ethers-decode-error'
const errorDecoder = ErrorDecoder.create()
const OverflowContract = new ethers.Contract('0x12345678', abi, provider)
try {
const tx = await OverflowContract.add(123)
await tx.wait()
} catch (err) {
const { reason } = await errorDecoder.decode(err)
console.log('Panic message:', reason)
}
Custom Errors
import { ErrorDecoder } from 'ethers-decode-error'
import type { DecodedError } from 'ethers-decode-error'
const abi = [
{
inputs: [
{
internalType: 'address',
name: 'token',
type: 'address',
},
],
name: 'InvalidSwapToken',
type: '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)
console.log('Custom error reason:', reason)
}
const customReasonMapper = ({ name, args }: DecodedError): string => {
switch (name) {
case 'InvalidSwapToken':
return `Invalid swap with token contract address ${args[0]}.`
return `Invalid swap with token contract address ${args['token']}.`
default:
return 'The transaction has reverted.'
}
}
Custom Errors ABI and Interfaces
Although the ABI or ethers Interface
object of the contract is not required when decoding normal revert errors, it is recommended to provide it if you're expecting custom errors. This is because the ABI or Interface
object is needed to decode the custom error name and parameters.
💡 You can provide ABIs and Interface
objects of multiple smart contracts where you expect custom errors. By doing so, you have a "universal" ErrorDecoder
within your codebase capable of decoding any contract errors thrown. This decoder can then be reused throughout your code to handle any errors.
If you're expecting custom errors from multiple contracts or from external contracts called within your contract, you can provide the ABIs or interfaces of those contracts:
const myContractAbi = [...]
const externalContractAbi = [...]
const errorDecoder = ErrorDecoder.create([myContractAbi, externalContractAbi])
try {...} catch (err) {
const decodedError = await errorDecoder.decode(err)
}
If you are using TypeChain in your project, it may be more convenient to pass the contract Interface
objects directly:
const errorDecoder = ErrorDecoder.create([MyContract.interface, MySecondContract.interface])
const errorDecoder = ErrorDecoder.create([
MyContract__factory.createInterface(),
MySecondContract__factory.createInterface(),
])
You can also mix both ABIs and contract Interface
objects, and the library will sort out the ABIs for you. This can be useful if you just want to append adhoc ABI of external contracts so that their errors can be recognised when decoding:
const externalContractFullAbi = [...]
const anotherExternalContractErrorOnlyAbi = [{
name: 'ExternalContractCustomError1',
type: 'error',
}]
const errorDecoder = ErrorDecoder.create([MyContract__factory.createInterface(), externalContractFullAbi, anotherExternalContractErrorOnlyAbi])
If the ABI of a custom error is not provided, the error name will be the selector of the custom error. In that case, you can check the selector of the error name in your reason mapper to handle the error accordingly:
const customReasonMapper = ({ name, args }: DecodedError): string => {
switch (name) {
case 'InvalidSwapToken':
return `Invalid swap with token contract address ${args[0]}.`
case '0xec7240f7':
return 'This is a custom error caught without its ABI provided.'
default:
return 'The transaction has reverted.'
}
}
Contributing
Feel free to open an issue or PR for any bugs/improvements.