Free 30-min Web3 Consultation
Book Now
Smart Contract Security Audits
Learn More
Custom DeFi Protocol Development
Explore
Full-Stack Web3 dApp Development
View Services
Free 30-min Web3 Consultation
Book Now
Smart Contract Security Audits
Learn More
Custom DeFi Protocol Development
Explore
Full-Stack Web3 dApp Development
View Services
Free 30-min Web3 Consultation
Book Now
Smart Contract Security Audits
Learn More
Custom DeFi Protocol Development
Explore
Full-Stack Web3 dApp Development
View Services
Free 30-min Web3 Consultation
Book Now
Smart Contract Security Audits
Learn More
Custom DeFi Protocol Development
Explore
Full-Stack Web3 dApp Development
View Services
LABS
Guides

How to Diagnose EVM Execution Failures

A systematic guide for developers to identify and fix failed transactions on Ethereum and EVM-compatible chains. Covers common error types, diagnostic tools, and code examples.
Chainscore © 2026
introduction
DEVELOPER GUIDE

How to Diagnose EVM Execution Failures

A systematic approach to identifying and understanding why your Ethereum transactions revert, from gas estimation to opcode-level debugging.

An EVM execution failure occurs when a transaction is processed by the network but the smart contract code does not complete successfully, resulting in a state reversion and the infamous "out of gas" or "revert" error. Unlike network-level failures (e.g., invalid nonce), these happen during contract execution. The primary diagnostic tool is the transaction receipt, which contains the status field (0 for failure, 1 for success) and, crucially, the revertReason or raw output data if the revert included an error message via require(), revert(), or custom errors.

The first and most common failure mode is an out of gas error. This doesn't always mean you provided insufficient gas; it can indicate an infinite loop, excessive storage operations, or a complex computation hitting the block gas limit. Use eth_estimateGas RPC calls during development to simulate execution and get a baseline. A significant discrepancy between the estimate and the actual gas used in a failed transaction often points to a divergent execution path, such as a loop running more times than expected due to unexpected input data.

When a transaction reverts with a reason, decoding this message is essential. For reverts using require(condition, "Error message") or revert("Error message"), the reason is a UTF-8 string. For modern custom errors (e.g., error InsufficientBalance()), the reason is an ABI-encoded selector. You can decode these using tools like the ethers.js Contract interface, web3.py, or online ABIs. A revert without a readable message often indicates a failed assertion (assert) or an opcode-level failure like division by zero.

For deep debugging, you need to examine the execution trace. Tools like the debug_traceTransaction RPC method (available on nodes like Erigon, Geth with debug flags) provide a step-by-step log of every opcode executed, stack, memory, and storage changes. This allows you to pinpoint the exact opcode where the failure occurred. Look for REVERT or INVALID opcodes. A failing SSTORE might indicate a gas cost miscalculation (e.g., a storage slot changing from zero to non-zero), while a failing CALL could show a failed low-level transfer.

Common failure patterns include: reentrancy guard violations, where a nonReentrant modifier throws; arithmetic overflows/underflows, which since Solidity 0.8.x trigger automatic reverts; incorrect function selectors or ABI encoding leading to unexpected jump destinations; and state dependency, where a transaction succeeds in isolation but fails in a different block state due to prior transactions. Using a forked mainnet environment with Tenderly or Foundry's cheatcodes can help replicate and diagnose these state-specific issues.

To systematically diagnose, follow this workflow: 1. Check the transaction receipt for status and revert data. 2. Decode any revert reason using your contract's ABI. 3. Simulate the transaction locally using eth_call on a forked chain to test different inputs. 4. If the reason is unclear, obtain and analyze a full execution trace. 5. Use development frameworks like Foundry (forge test --debug) or Hardhat (console.log) to add visibility. Understanding these failures is key to writing robust smart contracts and handling user transactions gracefully in your front-end application.

prerequisites
PREREQUISITES AND SETUP

How to Diagnose EVM Execution Failures

A guide to the essential tools and foundational knowledge required to effectively debug failed transactions and smart contract calls on Ethereum and other EVM-compatible chains.

Diagnosing EVM execution failures requires a solid understanding of the transaction lifecycle and the right tooling. Before you begin, ensure you are familiar with core concepts: transaction receipts, gas, opcodes, and revert reasons. A transaction can fail due to insufficient gas, a failed requirement check (like require()), an invalid state change, or an outright error in contract logic. The first step is always to obtain the transaction hash of the failed transaction, which serves as your primary identifier for all subsequent investigation.

Your primary toolkit should include a block explorer like Etherscan or its equivalents (e.g., Arbiscan, BscScan), a local development node (Hardhat or Foundry's Anvil), and the ability to decode raw transaction data. For advanced debugging, you'll need to interact with an EVM execution trace. This trace is a step-by-step log of every opcode executed during the transaction, showing stack, memory, and storage changes. Tools like Hardhat Network's console.log, Foundry's forge test --debug, and the Tenderly debugger are indispensable for generating and analyzing these traces.

Set up a local development environment to replicate failures. Using Foundry, you can fork the mainnet state to replay a transaction exactly as it occurred: anvil --fork-url $RPC_URL. Then, use cast run <txHash> --rpc-url http://localhost:8545 to execute the transaction locally and get a full trace. With Hardhat, you can use the hardhat node --fork command and then the network's traceTransaction RPC method. This local replay isolates the issue from network conditions and allows you to add custom logging or modify state to test hypotheses without spending real gas.

Understanding common revert opcodes is crucial. A REVERT opcode (0xFD) with returned data indicates a deliberate failure like a failed require or revert statement. An INVALID opcode (0xFE) typically signifies a more severe error, such as an invalid jump destination or stack underflow. Use cast or a library like ethers.js to decode the revert reason. For example, cast run --debug will show the exact opcode where execution halted, while cast receipt <txHash> --rpc-url $RPC_URL will show the raw revert data in the transaction receipt.

Finally, methodically analyze the failure. Start with the block explorer to check for an error message and gas usage. If the reason is unclear, move to local replay and trace analysis. Look for the point where the program counter jumps to the REVERT opcode and examine the stack and memory state immediately before. Common pitfalls include: incorrect function selectors, mismatched data types in ABI encoding, storage layout collisions, and gas estimation errors. By systematically applying these tools and concepts, you can diagnose the root cause of virtually any EVM execution failure.

initial-diagnosis-steps
FUNDAMENTAL PROCESS

Step 1: Initial Diagnosis and Common Errors

Systematically identify the root cause of a failed transaction by analyzing the transaction receipt and common execution patterns.

When an Ethereum transaction fails, the first step is to retrieve its receipt using a node RPC call like eth_getTransactionReceipt. The status field is your primary indicator: 0x0 means failure, 0x1 means success. A failed status, combined with zero gas used, often indicates a revert in the constructor during contract deployment. If gas was used, the execution failed within the contract logic itself. Always check the revertReason field if your node supports EIP-140/3670, as it may contain a human-readable error string from a require() or revert() statement.

The transaction receipt also contains the gasUsed and effectiveGasPrice, which help contextualize the failure. If gasUsed equals the transaction's gasLimit, you've hit an out-of-gas (OOG) error. This occurs when execution exhausts all allocated gas, often due to an infinite loop, an excessively large computation, or an underestimated gas limit for the operation. Distinguish this from a revert, which consumes gas up to the point of failure and then refunds the remainder. Tools like the Ethereum Execution Specification (EELS) can help trace the exact opcode where gas was exhausted.

Common execution failures stem from specific EVM opcodes. The REVERT opcode (0xFD) is a controlled failure triggered by Solidity's require(), revert(), or failed assert(). The INVALID opcode (0xFE) indicates a critical error, such as a jump to an invalid program counter or a stack underflow, often pointing to compiler bugs or corrupted bytecode. An assertion failure consumes all remaining gas, while a require failure refunds it. Understanding which opcode caused the stop is key to diagnosing compiler-level issues versus application logic errors.

Use structured error signatures to decode failures. Many contracts use Error(string) or Panic(uint256) as defined in EIP-838. A Panic error with code 0x11 (hex) indicates an arithmetic underflow/overflow (pre-Solidity 0.8.0), while 0x12 signals a division by zero. Libraries like ethers.js and web3.py can decode these errors from the revert data. For custom errors introduced in Solidity 0.8.4, you need the contract ABI to decode the Error(string) or custom error type, which provides precise, gas-efficient failure reasons.

Always simulate transactions before broadcasting. Use eth_call or eth_estimateGas on a pending block to test execution without spending gas. If eth_estimateGas fails, the transaction will certainly fail on-chain. Compare the estimated gas against the block gas limit and typical costs for the operation. A drastic discrepancy often reveals logic errors. For complex interactions, tools like Tenderly or OpenZeppelin Defender provide advanced simulation with full stack traces, pinpointing the exact line of Solidity code that caused the revert.

FAILURE CATEGORY

Common EVM Execution Failure Types

A breakdown of common execution failure types in the Ethereum Virtual Machine, their causes, and typical error messages.

Failure TypePrimary CauseTypical Error / Revert StringGas Consumed?

Out of Gas

Transaction gas limit exceeded before execution completes

"out of gas"

Revert

Explicit revert() or require() statement triggered

"execution reverted" or custom string

Invalid Opcode

Execution of an undefined or invalid opcode (0xFE)

"invalid opcode"

Stack Underflow

Popping more items from the stack than are available

"stack underflow"

Stack Overflow

Exceeding the maximum stack depth of 1024 items

"stack overflow"

Invalid Jump

JUMP/JUMPI destination is not a JUMPDEST opcode

"invalid JUMP"

Static State Change

Modifying state within a staticcall() context

"write protection"

Insufficient Balance

Transfer value exceeds contract or caller balance

No explicit error; transaction fails

decoding-revert-reasons
DIAGNOSING EVM EXECUTION FAILURES

Step 2: Decoding Revert Reasons and Custom Errors

When a transaction fails, the EVM provides a cryptic error code. This guide explains how to decode these messages to understand exactly why your smart contract call reverted.

When a transaction fails on the Ethereum Virtual Machine (EVM), it returns a revert reason—a piece of data that explains the failure. Prior to the Berlin hard fork (EIP-2929), contracts could only revert with a generic error. Since then, the require(), revert(), and assert() statements can include a human-readable string, which is ABI-encoded into the transaction's revert data. For example, require(balance >= amount, "Insufficient balance"); will encode the error string for off-chain tools to decode. This is your first clue in diagnosing a failed transaction.

The structure of revert data follows a specific pattern. The first 4 bytes are a function selector, known as an error selector, which identifies the type of error. For a custom error like error InsufficientFunds(uint256 available, uint256 required);, the selector is the first 4 bytes of the Keccak-256 hash of the error signature. The remaining data is the ABI-encoded parameters. You can decode this manually using libraries like ethers.js (ethers.AbiCoder) or web3.py, or use block explorers like Etherscan which automatically parse and display these errors for verified contracts.

Custom errors, introduced in Solidity 0.8.4, are a gas-efficient alternative to revert strings. They are defined with the error keyword and can include parameters. When such an error is thrown with revert CustomError(arg1, arg2);, it provides structured data instead of a string. Decoding them requires knowing the exact error signature used by the contract. This is why publishing your contract's ABI—which includes custom error definitions—is crucial for transparency and debugging. Tools like Hardhat and Foundry will display decoded custom errors in their test traces by default.

To programmatically decode a revert reason in your dApp frontend, catch the error from your library call and parse the data field. In ethers v6, you can use ethers.decodeErrorResult. You need the contract's interface, which includes the custom error fragments. For unverified contracts or unknown errors, you may only see the raw 0x data. In these cases, you can analyze the selector against potential function or error signatures using online databases or the 4byte directory. Understanding this flow is essential for building robust applications that handle transaction failures gracefully.

diagnostic-tools
EVM DEBUGGING

Essential Diagnostic Tools and Libraries

When a transaction fails, these tools and libraries help you analyze gas, trace execution, and inspect state to pinpoint the exact cause.

06

Gas Estimation & Analysis

Many failures are due to incorrect gas estimation. Use these methods to diagnose gas-related issues:

  • eth_estimateGas RPC call: Simulate the transaction to get a gas estimate; a failure here indicates a certain revert.
  • Gas profiling tools: Hardhat and Foundry can generate gas reports showing the cost of each function.
  • Block gas limit: Transactions requiring more than 30 million gas will fail on mainnet. Check for unbounded loops or excessive storage writes.
30M
Mainnet Block Gas Limit
gas-analysis-debugging
DIAGNOSING EXECUTION FAILURES

Step 3: Gas Analysis and Debug Traces

When an EVM transaction fails, gas analysis and debug traces are essential tools for identifying the root cause, whether it's an out-of-gas error, a failed assertion, or a reverted call.

The first step in diagnosing a failed transaction is to analyze its gas usage. A transaction can fail due to an out-of-gas error, where the provided gas limit is insufficient, or a revert, where the contract logic explicitly halts execution. Tools like Etherscan or block explorers show the gas used versus the gas limit. If gas used equals the limit, the failure is likely an out-of-gas condition, indicating your transaction ran out of computational budget. If gas used is less than the limit, the contract reverted, often with a custom error message defined by require(), revert(), or assert() statements.

To understand why a revert occurred, you need a debug trace. This is a step-by-step log of the EVM's execution, showing every opcode, stack operation, memory change, and storage access. You can generate a trace using an RPC method like debug_traceTransaction on nodes that support it (e.g., Geth, Erigon). The trace reveals the exact opcode where execution stopped. For example, a REVERT opcode will be present, and the data on the stack before it often contains the revert reason, which can be decoded from hexadecimal back to a string to read the error message.

Here is a simplified example of using curl to fetch a debug trace for a failed transaction hash on a local Geth node:

bash
curl -X POST --data '{"jsonrpc":"2.0","method":"debug_traceTransaction","params":["0xtx_hash_here", {}],"id":1}' http://localhost:8545

The response is a JSON object detailing the execution steps. Look for the last op field; if it's REVERT, the preceding stack and memory data hold the clue. For contracts compiled with Solidity >=0.8.4, this data often includes a human-readable string when using revert("Error message").

Beyond simple reverts, traces help diagnose complex failures like those in nested calls. A failed internal CALL or DELEGATECALL will bubble up a revert to the parent. The trace shows the call depth and gas passed to each sub-context. You can identify if a failure originated in an external protocol integration, a library, or your main contract. This is critical for debugging interactions with other DeFi protocols, where a failure in a token transfer or a price oracle call can cause your entire transaction to fail.

For advanced analysis, tools like Tenderly or the Hardhat Network provide enhanced, visual debuggers that map trace steps back to your Solidity source code. They highlight the exact line that caused the revert, display variable states at each step, and simulate "what-if" scenarios. Integrating these tools into your development workflow allows for rapid iteration and testing of transaction logic before deployment, significantly reducing debugging time in production environments.

DIAGNOSING EXECUTION

Step 4: Troubleshooting Specific Scenarios

This section addresses common EVM execution failures, providing developers with diagnostic steps and solutions for issues like gas estimation, revert errors, and state inconsistencies.

A generic execution reverted error without a message is the default behavior of Solidity's revert() and require() statements when no reason string is provided. To diagnose:

  • Check contract source: The failing function likely contains a require(condition) or if (!condition) { revert(); } statement.
  • Use a local fork: Replay the transaction on a forked mainnet using Foundry or Hardhat. Tools like Foundry's forge test --debug <TX_HASH> or Hardhat's console.log can expose the exact failing line.
  • Review custom errors: For contracts compiled with Solidity >=0.8.4, the error may be a custom error type (e.g., error InsufficientBalance()). You need the contract ABI to decode it. Use cast run --debug or the Tenderly debugger.
  • Gas estimation failure: Sometimes the RPC provider's gas estimation fails and returns this generic error. Try manually increasing the gas limit significantly to see if a more specific on-chain revert message appears.
using-tenderly-hardhat
TUTORIAL

Step 5: Advanced Debugging with Tenderly and Hardhat

Learn to diagnose complex EVM execution failures by combining Hardhat's local simulation with Tenderly's powerful transaction inspection tools.

When a transaction fails on-chain, the error message is often generic, like "execution reverted" or "out of gas." To diagnose the root cause, you need to replay the transaction's execution with full visibility into the EVM's state. Hardhat Network provides a local environment where you can fork the mainnet at a specific block and re-execute the failing transaction. This allows you to inspect state changes and use console.log in your Solidity code, which is invaluable for understanding the flow of execution before the point of failure.

For deeper inspection, Tenderly offers a powerful suite of debugging tools. After simulating a transaction in Hardhat, you can use the @tenderly/hardhat-tenderly plugin to push the transaction trace directly to Tenderly's dashboard. This gives you a visual, step-by-step breakdown of the call stack, state changes for every contract and storage slot, and the exact opcode where the transaction reverted. You can see the precise values of function arguments, internal calls, and emitted events that led to the failure.

A common workflow is to first isolate the issue locally. For example, if a complex DeFi interaction fails, fork mainnet and run the transaction using Hardhat's --fork flag. Add console.log statements to your contract or use Hardhat's console.log in your test to print variable states. If the local trace isn't sufficient, export the transaction data and simulate it in Tenderly. The dashboard will show you a detailed gas profile and highlight the specific REVERT or ASSERT_FAIL opcode, along with the exact line of Solidity that caused it.

Key tools to master include Tenderly's Gas Profiler, which identifies expensive function calls, and the State Diff view, which shows all storage modifications. For reentrancy or access control issues, the Call Trace is essential—it maps every internal transaction. Always compare the failing transaction against a successful one to spot deviations in logic or state. This combination of local iteration with Hardhat and deep forensic analysis with Tenderly transforms opaque blockchain errors into solvable engineering problems.

EVM DEBUGGING

Frequently Asked Questions

Common developer questions and solutions for diagnosing failed transactions, reverts, and unexpected behavior on the Ethereum Virtual Machine.

An "out of gas" error with a high limit typically indicates an infinite loop or an operation that consumes gas unpredictably. The EVM halts execution when the gas specified in gasLimit is exhausted, regardless of the gasPrice. Common causes include:

  • Unbounded loops: A while or for loop without a fixed upper bound that depends on user input or dynamic state.
  • External calls to unknown contracts: A low-level .call() to an address that could be a contract executing a lot of code or itself reverting after consuming gas.
  • Storage operations in loops: Writing to storage (sload/sstore) inside a loop is extremely gas-intensive.

Diagnosis: Use a debugger in Remix or Hardhat to step through execution. Check if a loop variable can be manipulated by a user to cause excessive iterations. Tools like Tenderly or OpenChain can provide a gas trace showing the exact opcode where gas ran out.

conclusion
KEY TAKEAWAYS

Conclusion and Best Practices

Diagnosing EVM execution failures requires a systematic approach. This guide outlines a final checklist and best practices to streamline your debugging workflow.

A structured diagnostic workflow is essential for efficiently resolving EVM execution failures. Start by verifying the transaction's status with eth_getTransactionReceipt to confirm a "status": "0x0". Next, retrieve the exact revert reason using eth_call with the same transaction parameters or by analyzing the raw trace data from debug_traceTransaction. This initial triage separates network issues from contract logic errors, saving significant time.

When analyzing revert data, remember that not all failures provide a human-readable message. For custom errors introduced in Solidity 0.8.4 and require() statements, decode the revert reason using the contract's ABI. For low-level revert opcodes or failed assertions, you must examine the execution trace. Tools like the Tenderly Debugger or OpenChain's trace viewer can visualize stack, memory, and storage changes at the failing opcode, revealing the root cause.

To prevent common failures, adopt proactive development practices. Implement comprehensive error handling using custom error types for gas-efficient and informative reverts. Use static analysis tools like Slither or MythX during development to catch vulnerabilities. Always simulate transactions on a forked mainnet environment using Foundry's forge create and cast or Hardhat's network forking before broadcasting to a live network.

Integrate monitoring and alerting into your production systems. Use services like Chainlink Functions or custom indexers to watch for failed transactions related to your contracts. Log critical state changes and expected revert reasons off-chain. Establishing these practices reduces mean time to resolution (MTTR) and improves the reliability of your decentralized application for end-users.

How to Diagnose EVM Execution Failures | ChainScore Guides