Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
RPC Troubleshooting12 min read

Ethereum Revert Reasons and Custom Errors: Decoding Why a Transaction or eth_call Fails

Learn how to decode Ethereum revert reasons and custom errors from JSON-RPC responses, including Error(string), Panic(uint256), and custom error selectors.

TL;DR

When an Ethereum transaction or eth_call reverts, the JSON-RPC response contains an ABI-encoded revert reason in the error data field. This article explains how to decode Error(string) (0x08c379a0), Panic(uint256) (0x4e487b71), and custom errors (bytes4 selector) using ethers or viem, and how to retrieve revert reasons from transaction receipts with status 0x0.

Direct Answer: How to Read a Revert Reason from JSON-RPC

When an Ethereum transaction or eth_call fails, the revert reason is not returned as a plain-text message. Instead, the EVM returns an ABI-encoded byte string inside the JSON-RPC error object. For a typical eth_call, the response looks like {"error":{"code":3,"data":"0x08c379a0..."}} where the data field contains the encoded reason. To decode it, you must know the type of revert: Error(string) (selector 0x08c379a0), Panic(uint256) (selector 0x4e487b71), or a custom error (a 4-byte selector). For custom errors, you need the contract ABI to map the selector to the error name and arguments. This guide walks through the mechanism and provides a runnable script to decode all three types.

If you are troubleshooting an RPC endpoint, see our Ethereum RPC node guide and RPC pricing for endpoint configuration. For broader context, the OnFinality Learn hub covers related topics like Ethereum RPC timeouts and rate limits.

  • Revert reasons are ABI-encoded, not human-readable strings.
  • The JSON-RPC error data field contains the encoded reason.
  • Custom errors require the contract ABI to decode.
  • Transaction receipts with status 0x0 indicate a revert, but the reason is not stored on-chain.

EVM Revert Mechanism and ABI Encoding

When a Solidity contract executes revert(), require(false), or hits an arithmetic error, the EVM rolls back all state changes and returns a reason to the caller. The reason is encoded according to the ABI specification. For Error(string), the encoding is the 4-byte selector 0x08c379a0 followed by a 32-byte offset to the string data, then the string length and UTF-8 bytes. For Panic(uint256), the selector is 0x4e487b71 followed by a 32-byte integer panic code. Custom errors, introduced in Solidity 0.8.4, are encoded as the 4-byte selector of the error signature, optionally followed by ABI-encoded arguments.

The Ethereum execution specification documents that a revert consumes all gas and returns the reason to the caller. Solidity's documentation on custom errors explains that custom errors are identified by their selector and can carry arguments. This is why decoding a custom error without the ABI is impossible—you only see a 4-byte selector like 0x9e8b2f3a.

For a deeper dive into the JSON-RPC surface, the Ethereum execution APIs describe the standard error format. Providers may wrap the error differently, but the data field is the key to decoding.

  • Error(string) selector: 0x08c379a0
  • Panic(uint256) selector: 0x4e487b71
  • Custom error: 4-byte selector of the error signature
  • Panic codes: 0x01 (assert), 0x11 (overflow/underflow), 0x12 (division by zero), etc.

JSON-RPC Error Surface: eth_call and eth_sendRawTransaction

When you perform an eth_call that reverts, the node returns a JSON-RPC error object. In geth, the error code is 3 and the message is execution reverted, with the revert data in the data field. Other clients may use different codes or messages, but the data field is standard. For example, a failed eth_call might return:

For eth_sendRawTransaction, the transaction is mined and the receipt shows status: '0x0' if it reverted. However, the revert reason is not stored on-chain; you must re-simulate the transaction with eth_call to retrieve the reason. Some providers offer a debug_traceTransaction method to get the revert reason, but this is not standard and may require archive node access.

Provider-specific wrapping can vary. For example, some providers may include the revert data in a data field at the top level or use a different error code. Always inspect the full error object. If you are using a dedicated endpoint, see our API service for details.

  • eth_call returns revert data in the error object's data field.
  • eth_sendRawTransaction receipts with status 0x0 indicate a revert, but the reason is not included.
  • Use eth_call to simulate the transaction and capture the revert reason.
  • Provider error codes may vary; rely on the data field.
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": 3,
    "message": "execution reverted",
    "data": "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a496e73756666696369656e740000000000000000000000000000000000000000"
  }
}

Decoding Error(string) and Panic(uint256) Manually

To decode an Error(string) revert manually, you can parse the data: take the first 4 bytes to confirm the selector 0x08c379a0, then read the next 32 bytes as the offset (usually 0x20), then the next 32 bytes as the string length, and finally the UTF-8 bytes. For Panic(uint256), the first 4 bytes are 0x4e487b71, and the next 32 bytes are the panic code as a uint256.

For example, the data 0x08c379a0... with the string "Insufficient balance" decodes to the message. Panic code 0x11 indicates an arithmetic overflow or underflow. Solidity's panic codes are documented in the Solidity docs.

While manual decoding is educational, using a library like ethers or viem is more reliable and handles edge cases.

  • Error(string) data layout: selector + offset + length + string bytes.
  • Panic(uint256) data layout: selector + 32-byte panic code.
  • Common panic codes: 0x01 (assert), 0x11 (overflow), 0x12 (division by zero).

Decoding Custom Errors with ABI

Custom errors are more complex because the 4-byte selector alone does not tell you the error name or arguments. You must have the contract ABI that includes the error definition. For example, if your contract defines error InsufficientBalance(uint256 available, uint256 required), the selector is computed from the signature InsufficientBalance(uint256,uint256). To decode, you need to match the selector against the ABI and then decode the arguments.

Libraries like ethers.js and viem provide decodeErrorResult functions that take the ABI and the revert data. For ethers v6, you can use Contract.interface.parseError(data) or Interface.parseError. For viem, use decodeErrorResult from viem. These functions automatically handle the selector lookup and argument decoding.

Without the ABI, you can only see the selector. This is a fundamental limitation: custom errors are not self-describing. Always keep your contract ABI handy for debugging.

  • Custom error selector is the first 4 bytes of the revert data.
  • Use the contract ABI to map the selector to the error name and arguments.
  • ethers v6: contract.interface.parseError(data)
  • viem: decodeErrorResult({ abi, data })

Reproducible Example: Node.js Script with viem

The following script demonstrates how to decode revert reasons using viem. It takes an RPC URL, a contract address, and a calldata (or a function call) that triggers a revert. The script performs an eth_call and decodes the error data. Replace the placeholders with your own values.

Expected output for an Error(string) revert would be Error message: Insufficient balance. For a custom error, it would print the error name and arguments. The script assumes you have a contract that reverts; you can also use a known sample like a token transfer that fails due to insufficient balance.

To test with a real contract, you can use a public endpoint like the one provided by OnFinality. For a list of public endpoints, see our Ethereum network page.

  • The script uses viem's decodeErrorResult to handle all error types.
  • You must provide the ABI for custom errors.
  • The script prints the decoded error or the raw selector if unknown.
import { createPublicClient, http, decodeErrorResult } from 'viem';
import { mainnet } from 'viem/chains';

const client = createPublicClient({
  chain: mainnet,
  transport: http('YOUR_RPC_URL')
});

// Example ABI fragment for a custom error
const abi = [
  {
    type: 'error',
    name: 'InsufficientBalance',
    inputs: [
      { name: 'available', type: 'uint256' },
      { name: 'required', type: 'uint256' }
    ]
  }
];

async function decodeRevert() {
  try {
    // This call will revert; replace with your own contract call
    await client.call({
      address: '0xContractAddress',
      data: '0x...' // calldata that triggers revert
    });
  } catch (error) {
    const data = error.data; // or error.cause.data depending on viem version
    if (data) {
      const selector = data.slice(0, 10); // 0x + 4 bytes
      if (selector === '0x08c379a0') {
        // Decode Error(string) using viem's decodeErrorResult with a generic ABI
        const decoded = decodeErrorResult({ abi: ['error Error(string)'], data });
        console.log('Error message:', decoded.args[0]);
      } else if (selector === '0x4e487b71') {
        const decoded = decodeErrorResult({ abi: ['error Panic(uint256)'], data });
        console.log('Panic code:', decoded.args[0]);
      } else {
        // Try custom error with provided ABI
        try {
          const decoded = decodeErrorResult({ abi, data });
          console.log('Custom error:', decoded.errorName, decoded.args);
        } catch (e) {
          console.log('Unknown custom error selector:', selector);
        }
      }
    } else {
      console.log('No revert data in error:', error);
    }
  }
}

decodeRevert();

Results Table: Error Type to Fix Mapping

The following table summarizes common revert scenarios and the recommended fixes. Use it as a quick reference when debugging.

Error TypeSelectorCommon CauseFix
Error(string)0x08c379a0require/revert with messageInspect message; fix condition
Panic(0x01)0x4e487b71assert failureCheck invariants
Panic(0x11)0x4e487b71Arithmetic overflow/underflowUse SafeMath or Solidity 0.8+ checks
Panic(0x12)0x4e487b71Division by zeroCheck divisor
Custom errorvariesBusiness logic errorDecode with ABI; inspect args
Out of gasN/AGas limit too lowIncrease gasLimit
Revert without dataN/ALow-level revertCheck contract logic

For out-of-gas errors, the JSON-RPC error may not include revert data. In that case, you need to increase the gas limit and retry. For more on timeouts and gas, see our Ethereum RPC timeouts article.

Limitations and Tradeoffs

Decoding revert reasons has several limitations. First, custom errors require the contract ABI; without it, you only see a selector. Second, some providers may truncate or wrap the revert data, especially for large strings. Third, not all failure modes return a revert reason—for example, out-of-gas errors may not include data. Fourth, transaction receipts with status 0x0 do not include the revert reason; you must re-simulate the transaction.

Additionally, the JSON-RPC error format can vary between clients and providers. While the data field is standard, the error code and message may differ. Always log the full error object for debugging.

For production debugging, consider using a dedicated RPC endpoint with archive data to replay transactions. See our public vs dedicated RPC endpoints guide for more.

  • Custom errors need ABI; otherwise only selector is visible.
  • Provider may wrap or truncate revert data.
  • Out-of-gas errors may not include revert data.
  • Receipts with status 0x0 do not contain the reason.

Next Steps and Further Reading

Now that you understand how to decode revert reasons, you can apply this knowledge to debug your smart contracts and RPC calls. For more Ethereum-specific troubleshooting, explore our Ethereum RPC node guide and related articles on rate limits and timeouts. If you need to query historical data, see Querying historical blockchain data.

For a comprehensive understanding of RPC endpoints and pricing, visit our API service and RPC pricing pages. The OnFinality Learn hub offers many more guides.

Remember to always test with a local or testnet node before relying on a public endpoint. Happy debugging!

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started