GOMTU Crypto
guidePart 47 of 47 in this guide

Ethereum Revert Data Explained: Error Strings, Panic Codes, and Custom Errors

Learn how Ethereum revert data is encoded, how Solidity Error, Panic, and custom errors differ, and why selectors alone are not trustworthy.

GOMTU
GOMTU
Crypto Research · Published · 10 min read
Share𝕏in
Ethereum Revert Data Explained: Error Strings, Panic Codes, and Custom Errors

A contract call fails, the block explorer shows a red status, and your RPC client returns a wall of hexadecimal data. That payload is often the most useful clue you have. Ethereum revert data can identify a reason string, a compiler panic, or a typed custom error—but it can also be empty, malformed, or deliberately forged. Reading it correctly connects blockchain infrastructure basics to practical Solidity debugging.

Think of a reverted call like a canceled delivery. Ethereum rolls the package back to the sender, but the courier may attach a machine-readable note explaining why. The note helps diagnose the failure; it is not proof of who wrote it or whether the explanation is honest.

This guide explains the EVM mechanism, the three common Solidity formats, safe decoding, error bubbling, and the limits that wallets, indexers, and contracts must respect. It is educational, not financial advice (NFA). Never sign or retry a value-moving transaction solely because an error decoder gives it a reassuring label.

What Ethereum Revert Data Is

Advertisement

The EVM REVERT instruction stops the current call, marks it as failed, rolls back its state changes, and exposes a selected byte range from memory as failure data. EIP-140 introduced that behavior so a contract could abort without automatically consuming all remaining gas and could return a reason to its caller.

Revert data is therefore bytes, not inherently a sentence. Solidity layers an Application Binary Interface (ABI) convention on top of those bytes. A tool that knows the expected error signature can decode the payload into a name and typed arguments.

The transport works like this:

  1. A callee decides it cannot continue and executes REVERT with a memory offset and length.
  2. Its state changes and logs in the reverted call frame are discarded.
  3. The caller sees a failed call and can access the complete failure payload through the return-data buffer defined by EIP-211.
  4. High-level Solidity normally propagates the failure; low-level call, delegatecall, and staticcall instead return a success flag plus bytes.
  5. An RPC simulator or development tool may surface those bytes and try to decode them.

This is different from transaction calldata. Calldata describes the request sent into a call. Revert data describes a failure returned out of it.

The Three Common Solidity Error Formats

Solidity commonly produces Error(string), Panic(uint256), or a user-defined custom error. All three use ABI-shaped payloads, but they communicate different classes of failure.

FormatTypical sourcePayload shapeBest interpretation
Error(string)revert("reason"), require(condition, "reason")4-byte selector plus an ABI-encoded dynamic stringHuman-readable application condition
Panic(uint256)Failed assert, checked arithmetic, invalid array operation4-byte selector plus a 32-byte codeCompiler-recognized internal fault class
Custom errorrevert MyError(args) or supported require form4-byte selector plus ABI-encoded typed argumentsCompact, structured application error
Empty or unknown bytesrevert(), exceptional halt, assembly, another language, malformed dataNo universal shapeFailure needs context; do not guess

Error strings

Error(string) uses selector 0x08c379a0, followed by normal ABI encoding for a dynamic string: an offset, a byte length, and padded UTF-8 data. A reason such as "Not authorized" is convenient for people and test assertions, but a long string increases deployed bytecode and the amount of revert data copied.

An error string remains untrusted input. A contract can return Error("transfer succeeded") while reverting, and a deeper contract can originate the message that eventually reaches the user.

Panic codes

Panic(uint256) uses selector 0x4e487b71. Solidity emits it for conditions that should not occur in bug-free code. The numeric code distinguishes categories. Common examples from the current Solidity error-handling documentation include:

CodeMeaning
0x01assert(false) or a failed assertion
0x11Arithmetic underflow or overflow outside unchecked
0x12Division or modulo by zero
0x31Calling .pop() on an empty array
0x32Out-of-bounds array, bytes, or slice access
0x41Excessive memory allocation
0x51Calling an uninitialized internal function pointer

A panic code narrows the failure class; it does not reveal the source line by itself. You still need verified source, compiler metadata, a trace, and the exact state used for execution.

Custom errors

A custom error is encoded like a function call with no destination: the first four bytes of keccak256("ErrorName(canonicalTypes)"), followed by ABI-encoded arguments. The Solidity custom-error documentation notes that a parameterless error needs only four bytes of payload.

error InsufficientBalance(uint256 available, uint256 required);
 
function withdraw(uint256 amount) external {
    uint256 available = balances[msg.sender];
    if (amount > available) {
        revert InsufficientBalance(available, amount);
    }
    balances[msg.sender] = available - amount;
}

Compared with a prose string, typed fields are easier for an interface to localize and act on. The contract ABI can describe the error name and argument types, while NatSpec can document their meaning without storing that prose in every revert payload.

How to Decode Revert Data Safely

Decoding should be an evidence chain, not a selector lookup followed by blind trust.

1. Preserve the raw bytes and execution context

Keep the chain ID, block tag or number, target address, sender, value, calldata, and raw error bytes. A simulation against latest can produce a different result after state changes. A successful retry also does not prove the earlier decoder was wrong.

2. Classify known built-in selectors

If at least four bytes are present, compare the prefix with Error(string) and Panic(uint256). Validate the remaining ABI layout before decoding. A matching prefix with truncated offsets or missing words is malformed data, not a valid reason.

3. Resolve custom errors against a trusted ABI

Use the verified ABI for the exact implementation and chain. If the destination is a proxy, determine which implementation handled that block's call. A public selector database can suggest a signature, but four-byte collisions mean it cannot prove one.

4. Treat unknown and empty data honestly

Empty data can accompany revert() without arguments, an out-of-gas exception, an invalid opcode, failed ABI decoding, insufficient balance before a call frame starts, or infrastructure that did not expose the payload. These causes are not interchangeable. Report “no decodable revert data” and use a trace or controlled reproduction rather than inventing a reason.

5. Bound work before copying or decoding

An untrusted callee can return a large payload. Contracts that blindly copy and bubble all of it can pay memory-expansion and copying costs. Apply a size policy in low-level routers and monitoring systems, preserve a hash or bounded prefix when appropriate, and never assume a payload is well formed merely because it is long.

Bubbling, Catching, and Low-Level Calls

High-level external calls normally bubble exceptions: if a nested call reverts and nothing catches it, the failure travels up and the outer call also reverts. Solidity try/catch can handle failures from external calls and contract creation.

try vault.withdraw(amount) returns (uint256 received) {
    emit WithdrawalCompleted(received);
} catch Error(string memory reason) {
    emit WithdrawalFailed(reason);
} catch Panic(uint256 code) {
    emit VaultPanicked(code);
} catch (bytes memory lowLevelData) {
    emit UnknownFailure(keccak256(lowLevelData));
}

The final bytes clause matters. catch Error and catch Panic do not cover custom errors, empty payloads, or malformed data. Solidity also limits try/catch to the external call or creation expression; it is not a general mechanism for catching failures from arbitrary internal code.

Low-level calls behave differently:

(bool ok, bytes memory data) = target.call(payload);
if (!ok) {
    // Decode only under a documented trust policy, or bubble carefully.
}

Ignoring ok can make a failed action appear successful. Automatically bubbling data preserves diagnostics, but it also makes an inner contract's bytes appear at the outer boundary. That provenance problem is central to safe error handling.

Why Revert Data Is Not Trustworthy Proof

The Solidity ABI specification explicitly warns never to trust error data. Any contract can construct bytes matching any error signature. An error may also come from several levels deeper than the address you called.

Imagine a restaurant repeating a supplier's complaint to a customer. The wording reaches the dining room, but it did not necessarily originate with the restaurant. Likewise, a proxy, router, token hook, callback, or malicious dependency can generate data that looks like the outer contract's declared custom error.

This creates practical limits:

  • A selector is not an authenticated identity. Error namespaces do not include the contract address or source file.
  • A decoded name is not a state fact. InsufficientBalance(1, 2) is a claim encoded by the reverter, not a proof of balances.
  • A reason is not authorization. Do not release funds, grant privileges, or choose a privileged branch solely from untrusted revert bytes.
  • A simulation is not inclusion. State, ordering, gas, and caller context can change before a real transaction executes.
  • A receipt does not store the reason. A standard Ethereum transaction receipt records status, gas, and logs, but not general revert data. Recovering a historical reason usually requires replay or trace infrastructure with the needed state.

Custom errors are excellent diagnostics when their provenance is already trusted. They are weak authentication signals.

Risks and Common Mistakes

Decoding with the wrong ABI

Proxy upgrades, same-selector collisions, wrong-chain addresses, and stale artifacts can all produce a plausible but incorrect label. Pin the implementation and block context.

Assuming every failure has a reason

Out-of-gas and other exceptional halts may leave no useful payload. A decoder should distinguish empty, malformed, recognized, and unknown data instead of reducing them all to “execution reverted.”

Treating caught failure as harmless

After catch, the failed sub-call's state is rolled back, but the caller continues. That may be intentional; it may also leave the outer operation in an unsafe partial workflow. Design and test the continuation path explicitly.

Returning attacker-controlled payloads without limits

Forwarders and proxies often bubble data to preserve behavior. Review maximum copy size and memory cost, especially when the callee is user-selected. The relationship between high offsets and quadratic expansion is covered in the EVM memory guide.

Exposing secrets in reason strings

Revert data can be observed by callers, RPC providers, debuggers, and simulation systems. Do not place private keys, credentials, sensitive personal data, or internal secrets in error messages.

Developer Checklist

  • Record raw revert bytes before applying a decoder
  • Keep chain, block, target, caller, value, and calldata context
  • Validate total length, offsets, and ABI bounds before decoding
  • Resolve custom errors from a trusted, implementation-specific ABI
  • Handle Error, Panic, custom, empty, malformed, and unknown cases
  • Check the success flag from every low-level call
  • Add a final low-level catch when using Solidity try/catch
  • Never use untrusted revert bytes as authentication or authorization
  • Bound copied and logged error data from untrusted callees
  • Test nested calls, proxy upgrades, out-of-gas paths, and forged selectors

FAQ

Does a reverted transaction consume gas?

Yes. REVERT rolls back state changes and preserves unused gas in the current call rather than automatically burning all of it, but computation and memory used before the revert still cost gas. An included failed transaction therefore charges for work performed.

Is revert data stored in the transaction receipt?

Not as a general receipt field. The receipt exposes success status, gas information, and logs that survived execution. Nodes or tracing services may replay the call or retain additional diagnostics, but that is separate from the consensus receipt.

Are custom errors always cheaper than strings?

They are designed to be compact, especially when they use few fixed-size arguments, and parameterless custom errors need only a selector. Total cost still depends on deployed code, argument count and types, call path, and memory use. Measure the compiled contract and realistic execution.

Can try/catch catch a custom error by name?

Current Solidity catch clauses directly distinguish Error(string), Panic(uint256), and a general bytes fallback. Use catch (bytes memory data) for custom or unknown error bytes, then decode only under a safe ABI and provenance policy.

Can two errors share one selector?

Yes. Selectors are only four bytes, so collisions are possible. Even without a collision, any contract can fabricate matching bytes. Use the exact address, implementation, chain, verified ABI, and trace context.

Primary Sources

The Bottom Line

Ethereum revert data is a structured diagnostic channel built on raw EVM bytes. Error(string) serves readable application failures, Panic(uint256) identifies compiler-defined fault classes, and custom errors provide compact typed context. None of them authenticates its author.

Preserve raw data, decode against the correct ABI, keep execution context, and treat empty or malformed payloads honestly. Most importantly, use errors to explain failure—not to prove identity, authorize value, or guarantee what a later transaction will do. This article is educational and not financial advice (NFA); test on the target chain, protect funds you cannot afford to lose, and do your own research (DYOR).

Advertisement

Keep learning

Explore related topics

More from GOMTU