Create your own
Lesson illustration

Custom Errors for Gas Optimization

Hello! Welcome to the first lesson of our third module, "Advanced Solidity & Secure Development."

Introduction

In the previous module, you mastered the modern testing toolchain, culminating in using test coverage analysis to ensure your test suites are comprehensive. You now have a robust process for verifying your contract's logic and finding bugs.

Now, we shift our focus from finding issues to preventing them and writing code that is not only correct but also efficient and secure by design. This module will equip you with some of the most important patterns and practices in modern Solidity development.

Our first topic is a feature that has become a cornerstone of efficient and developer-friendly contracts: custom errors. Before Solidity version 0.8.4, developers relied on require and revert with string messages to handle failed conditions. While functional, this approach is expensive and inflexible.

Learning Outcome: By the end of this lesson, you will be able to implement custom errors to replace require/revert strings for gas optimization and improved developer experience.

We will cover:

  • The two primary benefits of custom errors: gas savings and improved developer/user experience.
  • The syntax for defining and using custom errors, both with and without parameters.
  • How custom errors are being standardized to improve the ecosystem as a whole.

1. Why Use Custom Errors? The Case for Gas and Clarity

The introduction of custom errors in Solidity v0.8.4 was driven by two major shortcomings of using string-based reverts: they are expensive and they are not very expressive.

Custom Errors in Solidity

Let's start with the official announcement from the Solidity team. This blog post explains the original motivation behind introducing custom errors.

Read the first three paragraphs of the article, ending just before the 'Example' section. Focus on the two main problems with revert strings that custom errors solve.

As the article notes, custom errors provide a "convenient and gas-efficient way to explain to users why an operation failed." Let's break down these two key advantages.

Gas Optimization

Every piece of data in a smart contract, including the error messages you write, costs gas to deploy. When you use revert("Insufficient funds.");, that string is stored as part of the contract's bytecode. If you have many long, descriptive error messages, the deployment cost can add up.

More importantly, custom errors provide a significant gas advantage at runtime when the error occurs.

Custom Errors in Solidity

The same Solidity blog post provides an excellent 'in-depth' comparison that demonstrates the gas savings at a low level. You don't need to be a Yul expert to grasp the key difference.

Read the section titled 'Errors in Depth'. Compare the two Yul code blocks. Notice that the custom error (revert Unauthorized()) only needs to store a 4-byte 'selector' and revert with those 4 bytes. The string-based revert, however, needs to store the selector for Error(string), the location and length of the string, and the string data itself, ultimately reverting with 100 bytes in their example.

The takeaway is simple:

  • Custom Error: revert Unauthorized() compiles down to reverting with just the 4-byte function selector of the error (0x82b42900).
  • String Revert: revert("Unauthorized") compiles down to a much more complex operation that includes the generic Error(string) selector and the string data itself.

This difference makes custom errors significantly cheaper, especially when the revert condition is met.

Improved Developer and User Experience (DX/UX)

The second major benefit is the ability to pass dynamic data with your errors. With a revert string, you're limited to a static message. If a transfer fails, revert("Insufficient balance") tells you what happened, but not the details.

Custom errors allow you to provide crucial context. For example:
revert InsufficientBalance({ available: 100, required: 150 });

This is a game-changer for several reasons:

  • Debugging: Developers can immediately see the state that caused the failure without needing to add extra logging events.
  • Off-chain Tooling: Testing frameworks and block explorers can decode this structured data and display it clearly.
  • Front-End Applications: As a front-end developer, you can catch these structured errors and present a rich, informative message to the user (e.g., "Your transaction failed. You have 100 tokens, but you tried to send 150.") instead of showing a generic "Transaction reverted" message.

2. How to Implement Custom Errors

Now that you understand the "why," let's look at the "how." The syntax is straightforward and will feel familiar if you've used events.

This short video provides a great visual walkthrough of the implementation process.

Error | Solidity 0.8

This video from Smart Contract Programmer quickly demonstrates the syntax for declaring and using custom errors, including how to add parameters.

Please watch from 04:34 to the end (05:54). The first part shows the basic declaration and usage, and the second part demonstrates how to add parameters to log useful data.

Let's summarize the implementation with a concrete example.

Imagine a simple withdraw function in a contract.

Before: Using require

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Bank {
    mapping(address => uint256) public balances;

    function withdraw(uint256 amount) public {
        uint256 userBalance = balances[msg.sender];
        require(amount <= userBalance, "Insufficient balance for withdrawal");

        balances[msg.sender] -= amount;
        // send funds...
    }
    // ... other functions
}

After: Using a Custom Error
To refactor this, you follow two steps:

  1. Declare the error: Define the custom error at the contract level. We'll include parameters to provide context.
  2. Replace require with an if/revert block: Use a conditional check and revert with your new custom error if the condition fails.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Bank {
    mapping(address => uint256) public balances;

    // 1. Declare the custom error with parameters
    error InsufficientBalance(uint256 available, uint256 required);

    function withdraw(uint256 amount) public {
        uint256 userBalance = balances[msg.sender];
        
        // 2. Replace require with an if/revert block
        if (amount > userBalance) {
            revert InsufficientBalance({
                available: userBalance,
                required: amount
            });
        }

        balances[msg.sender] -= amount;
        // send funds...
    }
    // ... other functions
}

This new version is cheaper and provides much more useful information when the withdraw call fails.

3. Standardization: The Future of Error Handling

Custom errors are powerful, but their full potential is realized when the ecosystem agrees on common definitions. If every developer creates their own version of InsufficientBalance, front-end applications and tools would need to support dozens of variations.

This is where standards like EIP-6093 come in. Proposed by developers, including those from OpenZeppelin, this EIP suggests standard names and parameters for common errors in token contracts (ERC-20, ERC-721, etc.).

Defining Industry Standards for Custom Error Messages

This article from OpenZeppelin explains the importance of standardizing custom errors and introduces EIP-6093.

Read the first few sections of this article. Pay special attention to the screenshots that contrast the user experience before and after the adoption of standard custom errors. This powerfully illustrates the 'improved user experience' benefit.

The screenshots in the article perfectly illustrate the end goal. A vague, unhelpful message like ALERT: Transaction error. Exception thrown in contract. is replaced by a clear, actionable one: Error: You don't have enough balance to complete this transaction. Balance: 0.0000, Cost: 0.0001.

This standardization, built on top of the custom error feature, is a massive leap forward for the usability of dApps. By adopting these standards in your contracts, you make them more interoperable and easier for everyone to integrate with.

Conclusion

You've now learned how to use one of the most impactful features from recent Solidity versions. Moving away from require strings to custom errors is a modern best practice that results in cleaner, cheaper, and more user-friendly contracts.

Key Takeaways:

  • Custom errors replace revert strings to provide a more efficient and expressive way of handling failures.
  • They offer significant gas savings on both deployment and runtime by storing only a 4-byte selector instead of a full string.
  • They dramatically improve developer and user experience by allowing dynamic data to be passed with the error, enabling clear and contextual feedback.
  • The syntax involves declaring an error at the contract level and using it with the revert statement inside a function.
  • Ecosystem standards like EIP-6093 are making error handling consistent, which is crucial for tooling and front-end integration.

Next Steps:

Now that we've improved how our contracts fail, our next lesson will focus on preventing one of the most notorious vulnerabilities in smart contract history: reentrancy. We will learn about the Checks-Effects-Interactions pattern, a simple yet powerful design principle to safeguard your contracts against this common attack vector.

Can't find a good explanation? Sign up and we'll make it for you

Sign up