starname.me

How to debug execution reverted error on Ethereum RPC

You send a transaction. It fails. The only message you get back is "execution reverted." No reason string. No clue what went wrong. This is one of the most frustrating things in Ethereum development.

The error happens when a smart contract's require or revert statement executes, but the contract did not include a reason string. Alternatively, a custom error was thrown, and your tool does not display it. The nodes/rpc-endpoint-basics/">RPC endpoint returns a generic revert because eth_call simulates the transaction and only knows that execution halted.

Let us walk through how to get the actual revert reason.

Why eth_call returns a generic revert

When you call eth_call, the node runs the transaction locally. If the contract reverts, the node returns a response with the code 0x and an error message that typically says "execution reverted." That is it. The node does not decode the revert data for you. It passes back the raw bytes from the contract.

The raw revert data is in the data field of the error response. You need to decode it yourself.

Getting the revert reason from a simulated call

If you are using a JavaScript library like ethers.js or web3.js, you can catch the error and inspect its data property. The data is a hex string. The first four bytes (if present) are the error signature. The rest is the encoded reason string or custom error arguments.

For a simple string revert, the data will look like this:

0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000185468652072657665727420726561736f6e20737472696e670000000000000000

The first four bytes 08c379a0 are the Keccak-256 hash of Error(string). The rest is an ABI-encoded string.

You can decode this with ethers.js:

ethers.utils.defaultAbiCoder.decode(['string'], '0x' + error.data.slice(10))

For web3.js:

web3.eth.abi.decodeParameter('string', '0x' + error.data.slice(10))

Getting the revert reason from a mined transaction

If the transaction was already mined and reverted, you cannot use eth_call. You need debug_traceTransaction. This method requires a node with debug namespace access. Geth and Erigon expose it. Infura and Alchemy do not for the general public.

Call debug_traceTransaction with the transaction hash and the tracer set to callTracer. The response includes a revertReason field if the contract reverted with a reason string.

Example:

{
 "jsonrpc": "2.0",
 "id": 1,
 "method": "debug_traceTransaction",
 "params": ["0x...", {"tracer": "callTracer"}]
}

The result will have "revertReason": "the reason string" at the top level or nested inside the last call frame.

If revertReason is empty or missing, the contract reverted without a reason string. It may have thrown a custom error or a panic code.

Custom errors and panic codes

Solidity 0.8.4 introduced custom errors. They are not strings. They are typed errors with named arguments. The revert data for a custom error looks like this:

0x9e4c5e6b000000000000000000000000000000000000000000000000000000000000002a

The first four bytes are the error signature. You need the contract ABI to decode the arguments.

Tools like Tenderly and Foundry cast handle this automatically. Tenderly's debugger shows the custom error name and decoded arguments. Foundry cast can decode with the --revert flag:

cast run <tx_hash> --revert

It parses the revert data using the contract's ABI.

Panic codes are different. They occur when an assertion fails or an arithmetic overflow happens. The revert data starts with 0x4e487b71, the selector for Panic(uint256). The encoded uint256 is the panic code. Common codes: - 0x01: failed assertion - 0x11: arithmetic underflow or overflow - 0x12: division by zero - 0x32: array out-of-bounds access

Decode a panic code the same way as a custom error: with the ABI for Panic(uint256).

Distinguishing simulation revert vs on-chain revert

A simulation revert never hits the blockchain. It is local. Your gas is not consumed. The state does not change. You can retry with different inputs.

An on-chain revert consumed gas up to the point of failure. The gas is spent. The transaction is in a block. The state is unchanged, but you paid for the failed execution.

The same methods work for both, but the debugging urgency differs. For a simulation revert, you can iterate quickly. For a mined revert, you want to avoid paying for the mistake again.

Practical debugging flow

  1. Catch the error from eth_call or eth_estimateGas. Extract the data field.
  2. If the data starts with 08c379a0, decode it as Error(string).
  3. If it starts with 4e487b71, decode it as Panic(uint256).
  4. Otherwise, check if it matches a custom error in your contract ABI. Use the first four bytes as a selector.
  5. If the transaction is mined and you have debug node access, run debug_traceTransaction with callTracer.
  6. If you have no debug access, use Tenderly or a local Foundry fork to simulate the transaction with cast run.

No tool can decode a revert from a contract whose ABI you do not have. The contract may have been written in Vyper or a private Solidity version. In that case, the raw revert data is all you get. You would need to inspect the contract bytecode.

The key takeaway: "execution reverted" is never the final word. The data is there. You just need to decode it.

Not financial advice. starname.me publishes market data and general information about digital assets. Crypto assets are volatile and you can lose everything you put in. Nothing here is a recommendation to buy, sell or hold, and we make no price predictions.

Prices are sourced from third parties and may be delayed or wrong. Verify anything you intend to act on against a primary source.

Back to rpc & nodes