Skip to main content
Create your own

Solidity Error Handling: `require`, `revert`, and Custom Errors

Welcome back! In our last lesson, we focused on structuring data within a contract using structs and enums, which is crucial for building clean data models. Now, we'll address the equally important task of enforcing the rules and logic that govern this data.

This lesson is about handling errors effectively in Solidity. Just as data warehousing and ETL processes rely on validation rules to ensure data integrity, smart contracts need robust error-handling mechanisms to protect assets, enforce business logic, and prevent unexpected behavior. For your goal of building systems for tokenized money, this is non-negotiable; a single unhandled error could lead to significant financial loss. We will cover the three main tools at your disposal: require, revert, and the modern, gas-efficient approach of custom errors.

By the end of this lesson, you will be able to write secure contracts that gracefully handle invalid inputs and failed conditions, a fundamental skill for any blockchain developer.

1. The Foundation of Solidity Error Handling

Before diving into the specific tools, it's essential to understand what happens when an error is triggered during a transaction. The Ethereum Virtual Machine (EVM) enforces a critical rule: if an error occurs, the transaction is halted, and all changes made to the contract's state (i.e., updates to state variables) during that transaction are rolled back, or reverted.

The video below explains this core concept and introduces the main error-handling statements.

Error | Solidity 0.8

This video from Smart Contract Programmer provides a concise overview of what happens when an error is thrown in Solidity.

Watch these two key segments: The initial explanation covers the two most important consequences of an error: state reversion and gas refund. The demonstration shows a practical example of a state variable (num) being incremented before an error is thrown, illustrating how that change is undone.

The key principle is that smart contract execution is atomic. Either the entire transaction succeeds and state changes are saved, or it fails and the state is left as if the transaction never happened. While the state is reverted, the gas consumed up to the point of the error is still paid to the network validators.

2. require() and revert(): The Classic Tools

For years, require() and revert() have been the standard for error handling. They serve slightly different syntactic purposes but achieve the same outcome.

  • require(condition, "Error message"): This is the most common way to handle errors. It is designed to check conditions, typically on inputs or state, at the beginning of a function. If the condition is false, it reverts the transaction and provides the error message. It's primarily used for validating user inputs and access control.

  • revert("Error message"): This is a direct instruction to revert the transaction. It is functionally equivalent to require(false, "Error message") but is more suitable inside complex conditional logic, like nested if/else statements, where a require statement might be less readable.

The following video segments clearly demonstrate how and when to use each.

Error | Solidity 0.8

This video continues by showing practical examples of require and revert.

Please watch these two sections: The require example demonstrates its use for input validation, a very common pattern. The revert example shows how it can be used within an if block, explaining why it's sometimes a better choice for more complex logic.

A Note on assert()
You may also see assert(condition). This function is semantically different from require. You should use require for conditions that can reasonably be expected to be false due to invalid external input (e.g., a user trying to withdraw more than their balance). You should use assert for conditions that should never be false if the code is correct. A failed assert typically indicates a bug in your contract. In modern Solidity, both require and assert refund unused gas, but their intended uses remain distinct. For day-to-day development, you will almost always use require.

3. Custom Errors: Gas-Efficient and Descriptive

While require(condition, "string message") is effective, it has a drawback: storing and returning error strings consumes a significant amount of gas, and gas costs on Ethereum are paramount. Since version 0.8.4, Solidity introduced custom errors as a cheaper and more expressive alternative.

Instead of a generic string, you can define a named, structured error, much like defining a struct. This is more gas-efficient because the EVM only needs to handle a 4-byte error selector instead of a variable-length string. Your experience with structured data in data warehousing is analogous here; structured, named errors are far more useful for tooling and debugging than raw text strings.

A Walkthrough of Solidity Custom Errors - DEV Community

This article clearly explains the motivation behind custom errors and their basic syntax.

Please read the first two sections of the article: Why Use Custom Errors? and <tf start="Syntax and Structure of Custom Errors" end="CustomErrorName("Reason", 42))">Syntax and Structure. Focus on the gas optimization benefit and the error keyword syntax.

To use a custom error, you typically pair it with the revert statement. The video below shows how to refactor a function from using require with a string to revert with a custom error.

Error | Solidity 0.8

This final segment from the "Error | Solidity 0.8" video demonstrates how to declare and use a custom error.

Watch the entire section on custom errors. Pay close attention to how a custom error is declared and then used with revert. Notice how you can pass arguments to the error, providing rich context for debugging, just like with events.

Let's look at a complete example. The SimpleBank contract below is a perfect illustration of custom errors in a financial context.

A Walkthrough of Solidity Custom Errors - DEV Community

This part of the article provides a practical example of a contract using a custom error.

Read the section Practical Example. Notice how the InsufficientBalance error is defined with parameters that give specific details about why the transaction failed (the user's address, the requested amount, and the available balance). This is far more informative than a simple "Insufficient funds" string.

The image below shows what this looks like in practice. A function mint() is reverting with a custom error Unauthorized, passing the caller's address as an argument. The console log shows the raw return data, which consists of the error selector (0x8e4a23d6) and the ABI-encoded address.

A smart contract in the Remix IDE reverting with a custom error `Unauthorized(msg.sender)`. The highlighted console output shows the encoded error data returned by the failed transaction.

4. Advanced Usage: require with Custom Errors

As of May 2024, Solidity now allows you to use custom errors directly with require, combining the conciseness of require with the gas efficiency of custom errors.

require(amount <= balance, InsufficientBalance(msg.sender, amount, balance));

This feature, however, currently requires enabling a specific compiler setting called viaIR, which uses a different compilation pipeline to generate more optimized bytecode. Since you are using Hardhat, you can enable this by adding the viaIR: true flag to your hardhat.config.js file.

// hardhat.config.js
module.exports = {
  solidity: {
    version: "0.8.26", // or your desired version
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
      viaIR: true, // Enable the via-IR pipeline
    },
  },
};

The following reading explains how require works and details how to enable via-ir to use it with custom errors.

Try Catch and all the ways Solidity can revert

This article from RareSkills dives deep into the mechanics of reverts. These sections cover the require statement and the new ability to use custom errors with it.

First, read section 4, on the require statement. This reinforces what you already know about how it works with and without a string message. Next, read section 5, on require with custom errors. Pay special attention to the "Enabling via-ir in Hardhat" part, which provides the exact configuration you'll need.

Conclusion

You now have a complete toolkit for handling errors in Solidity, from the classic require and revert statements to modern, gas-efficient custom errors. Properly validating inputs and state is one of the most critical aspects of smart contract security, especially when dealing with financial assets.

Here are the key takeaways:

  • Errors cause state reversion: All state changes are undone if a transaction fails.
  • require() is for validation: Use it to check conditions on inputs and state, such as verifying permissions or checking for sufficient funds.
  • revert() is for complex logic: Use it when error conditions are determined within nested if/else blocks.
  • Custom errors are the new standard: They are significantly more gas-efficient than error strings and provide structured, machine-readable data for debugging, making them ideal for production contracts.

In this lesson, we touched upon gas optimization as a key reason for using custom errors. In our next lesson, we will explore this topic in much greater detail by learning to distinguish between storage, memory, and calldata data locations. Understanding how Solidity handles data is fundamental to writing efficient and cost-effective smart contracts.

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

Sign up