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.

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
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:
- A callee decides it cannot continue and executes
REVERTwith a memory offset and length. - Its state changes and logs in the reverted call frame are discarded.
- The caller sees a failed call and can access the complete failure payload through the return-data buffer defined by EIP-211.
- High-level Solidity normally propagates the failure; low-level
call,delegatecall, andstaticcallinstead return a success flag plus bytes. - 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.
| Format | Typical source | Payload shape | Best interpretation |
|---|---|---|---|
Error(string) | revert("reason"), require(condition, "reason") | 4-byte selector plus an ABI-encoded dynamic string | Human-readable application condition |
Panic(uint256) | Failed assert, checked arithmetic, invalid array operation | 4-byte selector plus a 32-byte code | Compiler-recognized internal fault class |
| Custom error | revert MyError(args) or supported require form | 4-byte selector plus ABI-encoded typed arguments | Compact, structured application error |
| Empty or unknown bytes | revert(), exceptional halt, assembly, another language, malformed data | No universal shape | Failure 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:
| Code | Meaning |
|---|---|
0x01 | assert(false) or a failed assertion |
0x11 | Arithmetic underflow or overflow outside unchecked |
0x12 | Division or modulo by zero |
0x31 | Calling .pop() on an empty array |
0x32 | Out-of-bounds array, bytes, or slice access |
0x41 | Excessive memory allocation |
0x51 | Calling 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
catchwhen using Soliditytry/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
- EIP-140: REVERT instruction
- EIP-211: RETURNDATASIZE and RETURNDATACOPY
- Solidity: Error handling
- Solidity: Custom errors
- Solidity ABI specification: Errors
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).
Keep learning

Ethereum Calldata Explained: How to Decode Transaction Input Data
Learn how Ethereum calldata encodes function selectors and arguments, how explorers decode it, and what to verify before signing a contract transaction.

Ethereum Transaction Receipts Explained: Status, Gas, Logs, and Proofs
Learn how Ethereum transaction receipts record execution status, gas used, logs, contract creation, typed transactions, and receipt-root commitments.
Ethereum Proxy Contracts Explained: Delegatecall, UUPS, and Upgrade Risks
Learn how Ethereum proxy contracts use delegatecall, how transparent, UUPS, and beacon proxies differ, and how to verify upgrade authority safely.
Explore related topics

Ethereum Glamsterdam Upgrade: ePBS, BALs, and What Is Actually Planned
Ethereum Glamsterdam is expected in Q4 2026. Learn its frozen scope, Sepolia milestone, ePBS, block-level access lists, and remaining uncertainties.
Crypto Address Poisoning: How to Verify Wallet Addresses Before You Send
Crypto address poisoning plants a lookalike address in your transaction history. Learn how it works and follow a safer verification checklist.