Ethers.js – How to Handle Solidity Custom Errors Efficiently

custom-errorsethers.js

Since Solidity v0.8.4, custom errors are supported, also in ethers.js.

How can I use them in a test? Here is an example:

pragma solidity 0.8.10;

contract Contract {
    uint8 value;

    error ComparisionFailed(uint8 expected, uint8 actual);

    function setValue(uint8 _value) external {
        value = _value;
    }

    function compare(uint8 _value) external view {
        if (_value != value)
            revert ComparisionFailed({expected : value, actual : _value});
    }
}

The test below works, but this is a bad solution.

import {expect} from 'chai';
import {ethers} from 'hardhat';

describe('Contract', function () {
  let contract: any;

  beforeEach(async function () {
    let Contract = await ethers.getContractFactory('Contract');
    contract = await Contract.deploy();
  });

  it('Should revert if value is wrong', async function () {
    const expected = 123;

    await contract.setValue(expected);

    let actual = 123;
    await expect(
        contract.compare(actual)
    ).to.be.ok;

    actual = 1
    await expect(
        contract.compare(actual)
    ).to.be.revertedWith("custom error 'ComparisionFailed(123, 1)'"); // bad solution
  });
});

What's the proper way to handle this and check for the error ComparisionFailed from the abi and the values 123 and 1?

Best Answer

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])
Related Topic