Custom Errors in Solidity: A Gas-Efficient Alternative

Custom errors are a new feature in Solidity v0.8.4 that allow developers to define custom error types.

Author Avatar

wonjoon

  ·  4 min read

Custom Error #

Solidity v0.8.4 introduced custom errors, a new way to handle errors in smart contracts. Instead of using string-based revert messages, developers can now define error variables and use them efficiently.

Why Use Custom Errors? #

  • Lower gas costs compared to traditional string-based errors.
  • Reusability in external interfaces or libraries.
  • Easier error management across multiple smart contracts.

Comparison: String-Based vs. Custom Errors

// Before (Traditional Revert)
revert("Insufficient funds."); // No predefined error

// After (Custom Error)
error Unauthorized();   // Declare the error
revert Unauthorized();  // Use the error

How to Use Custom Errors in Solidity #

1. Using revert with Custom Errors #

Custom errors function similar to Solidity’s event mechanism, but must be used with the revert statement.

// Example: Custom Error for Unauthorized Access
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

error Unauthorized(); // Custom error

contract VendingMachine {
    address payable owner = payable(msg.sender);

    function withdraw() public {
        if (msg.sender != owner)
            revert Unauthorized();  // Using custom error with revert statement

        owner.transfer(address(this).balance);
    }
}
  • revert halts execution and returns the error.
  • As of Solidity v0.8.4, require does not support custom errors (Issue in github).

Equivalent Code Transformation between revert and require:

// Traditional require statement:
require(condition, "error message");

// Translates to:
if (!condition) revert CustomError();

2. Custom Errors with Parameters #

Custom errors can accept parameters, allowing developers to pass relevant data.

// Example: Custom Error with Parameters
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

/// Error: Insufficient balance for transfer.
/// @param available balance available.
/// @param required requested amount to transfer.
error InsufficientBalance(uint256 available, uint256 required);

contract TestToken {
    mapping(address => uint256) balance;

    function transfer(address to, uint256 amount) public {
        if (amount > balance[msg.sender])
            revert InsufficientBalance({
                available: balance[msg.sender],
                required: amount
            });

        balance[msg.sender] -= amount;
        balance[to] += amount;
    }
}
  • The error is ABI-encoded: abi.encodeWithSignature("InsufficientBalance(uint256,uint256)", balance[msg.sender], amount).
  • Helps return specific error information to external applications.

How Much Gas is Saved? #

Using truffle-contract-size, we compared contract sizes with and without custom errors.

// Example: Vending Machine Contract
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

error Unauthorized(); // Custom error

contract VendingMachine {
    address payable owner = payable(msg.sender);

    function withdraw() public {
        if (msg.sender != owner)
            revert Unauthorized();  // With custom error
            revert("Insufficient funds."); // Without custom error

        owner.transfer(address(this).balance);
    }
}

Contract Size Results:

MethodContract Size
With Custom Errors0.33 KiB
Without Custom Errors0.46 KiB

Even for simple errors, custom errors reduce contract size by ~0.13 KiB. For larger smart contracts, the savings are even more significant.

Comparing Yul Code: Custom Errors vs. String Errors #

Yul is an intermediate representation for Solidity bytecode.

1. Custom Error: revert Unauthorized() #

# revert Unauthorized();
let free_mem_ptr := mload(64)
mstore(free_mem_ptr, 0x82b4290000000000000000000000000000000000000000000000000000000000)
revert(free_mem_ptr, 4)
  • 0x82b42900: Custom error selector.
  • Minimal gas cost due to compact encoding.

2. String-Based Error: revert(“Unauthorized”) #

# Example: Decoding InsufficientBalance Error
# revert("Unauthorized");
let free_mem_ptr := mload(64)
mstore(free_mem_ptr, 0x08c379a000000000000000000000000000000000000000000000000000000000)
mstore(add(free_mem_ptr, 4), 32)
mstore(add(free_mem_ptr, 36), 12)
mstore(add(free_mem_ptr, 68), "Unauthorized")
revert(free_mem_ptr, 100)
  • 0x08c379a0: String error selector.
  • More storage operations, higher gas consumption.
  • Longer execution time compared to custom errors.

Decoding Custom Errors with ethers.js #

Custom errors can be decoded in ethers.js to retrieve detailed error information.

import { ethers } from 'ethers';

// Define an interface to match the custom error
const abi = [
  'function InsufficientBalance(uint256 available, uint256 required)',
];

const interface = new ethers.utils.Interface(abi);
const error_data =
  '0xcf479181000000000000000000000000000000000000' +
  '0000000000000000000000000100000000000000000000' +
  '0000000000000000000000000000000000000100000000';

const decoded = interface.decodeFunctionData(
  interface.functions['InsufficientBalance(uint256,uint256)'],
  error_data
);

console.log(
  `Insufficient balance for transfer. ` +
  `Needed ${decoded.required.toString()} but only ` +
  `${decoded.available.toString()} available.`
);
// Output: Insufficient balance for transfer. Needed 4294967296 but only 256 available.
  • ethers.js can decode ABI-encoded errors for better debugging.
  • Error parameters are extracted and converted into human-readable messages.

Caution: external call & Custom Errors #

Smart Contract Compilation & External Calls #

When compiling a contract:

  • The Solidity compiler includes all defined custom errors in the contract’s ABI.
  • However, external calls do not include errors from other contracts.

Security Concern: Manipulated Error Messages #

  • Malicious contracts can fake errors by crafting misleading return messages.
  • Developers must verify whether the error originates internally or externally.
  • Use NatSpec documentation to provide clear explanations for custom errors.

Conclusion #

Why Use Custom Errors:

  • Lower Gas Costs – Reduces contract size & execution gas.
  • Improved Debugging – Encodes detailed error information.
  • Better Maintainability – Centralized error management across contracts.
  • Efficient ABI Encoding – Works seamlessly with Solidity tooling & ethers.js.

When Should You Use Custom Errors:

  • Large contracts with complex logic.
  • Contracts where gas efficiency is critical.
  • Reusable libraries and interfaces.