callTracer) and what state did it read or change (prestateTracer). Both run inside the node’s EVM as it replays the transaction, and both are passed the same way — as a tracer field to debug_traceTransaction, debug_traceCall, debug_traceBlockByNumber, or debug_traceBlockByHash. This page explains how to read their output.
For which chains expose debug and trace APIs and how to enable them, see Debug and Trace APIs. For writing your own tracer, see Mastering custom JavaScript tracing.
Which tracer answers which question
| You want to know… | Use | Output shape |
|---|---|---|
| What contracts a transaction called, in what order, and why it reverted | callTracer | A nested tree of call frames |
| Where the gas went, per call | callTracer | gasUsed on each frame |
| What accounts and storage slots a transaction reads (to replay or simulate it) | prestateTracer (default mode) | Account state before execution |
| What state a transaction changed | prestateTracer with diffMode: true | pre/post of only what changed |
tracerConfig object, shown per tracer below.
callTracer: the call tree
callTracer reconstructs the transaction as a tree of call frames — the top-level call plus every CALL, DELEGATECALL, STATICCALL, CREATE, CREATE2, and SELFDESTRUCT it made.
cURL
calls array holds its subcalls, each of which may have its own calls. Here is a complete trace of a straightforward ERC-20 (USDT) transfer — one frame, no subcalls:
JSON
type— the opcode that created the frame.DELEGATECALLruns another contract’s code in the caller’s context (proxies), so a frame’sfrom/tobeing equal usually means a proxy delegating to its implementation.from/to— caller and callee.tois absent onCREATE/CREATE2frames (the address doesn’t exist yet).value— wei transferred with the call.gas/gasUsed— gas supplied to the frame and gas actually consumed by it (both hex). SumminggasUseddown a branch shows where a transaction spent its gas.input/output— calldata sent and data returned. The first 4 bytes ofinputare the function selector.calls— the subcalls this frame made, in execution order. Absent if the frame made none.
calls. This trace is an ETH transfer into a smart-contract wallet that DELEGATECALLs its implementation — note the top frame’s empty input (0x, a plain value transfer), the value carried on both frames, and the DELEGATECALL type on the subcall:
JSON
Reverts
When a call reverts, its frame carries anerror field, and — when the revert returned a reason string — a decoded revertReason plus the raw ABI-encoded output. This is a USDC transfer that reverts on insufficient balance; USDC is a proxy, so the revert propagates from the implementation (DELEGATECALL) up to the top frame:
JSON
error; its parents show execution reverted too as the failure propagates up. This is the fastest way to find which internal call failed rather than just seeing the top-level transaction fail. (revertReason is only present when the contract reverts with a reason string — some contracts, like USDT, revert with the INVALID opcode instead, which shows as "error": "invalid opcode: INVALID" with no revertReason.)
callTracer config
PasstracerConfig alongside the tracer:
JSON
onlyTopCall(defaultfalse) — whentrue, traces only the top-level call and skips all subcalls. Cheaper when you only need the entry call.withLog(defaultfalse) — whentrue, adds alogsarray (the events emitted) to each frame that emitted any. Useful for tying emitted events to the exact call that produced them.
prestateTracer: the state a transaction touches
prestateTracer has two modes, selected by diffMode.
Default mode: what the transaction reads
Called without config,prestateTracer returns every account the transaction touched, with its state as it was before the transaction ran — keyed by address:
cURL
JSON
balance (hex wei), nonce (a number), code (present for accounts that have code — the first account here carries a short EIP-7702 delegation), and, for contract accounts, a storage map of the exact slots read. This is the state needed to replay the transaction off-chain — feed it to a local EVM or an eth_call state override to reproduce execution without an archive node.
diffMode: what the transaction changed
WithtracerConfig: {"diffMode": true}, the tracer returns two maps, pre and post, containing only the accounts and fields that changed — pre holds the old values, post the new ones:
Here is the same ETH transfer from above, in diffMode:
JSON
post only for the fields that changed: the sender (0xa9ac43…) spent value and its nonce incremented; the recipient (0xe5198a…) and the fee recipient (0x4838b1…) only gained balance, so their nonce and code are omitted from post. An account created by the transaction appears only in post; one destroyed appears only in pre. This mode is the precise answer to “what did this transaction modify.”
When a transaction changes contract storage, the changed slots appear under storage in post. For example, an ERC-20 transfer’s post includes the token contract with the two balance slots it rewrote:
JSON
Applying tracers to other methods
The same{tracer, tracerConfig} object works across the debug namespace:
debug_traceCall— trace a hypothetical, unmined call (great for “why would this revert” before sending). Takes a call object and a block tag, then the tracer object.debug_traceBlockByNumber/debug_traceBlockByHash— apply the tracer to every transaction in a block; the result is an array of traces.
cURL
Tracing replays historical execution, so it needs the state at the traced block. Recent blocks work on a global node; tracing older history needs the state retained by an archive node. See Debug and Trace APIs for per-protocol availability and how to enable the namespaces.
FAQ
What is the difference between callTracer and prestateTracer?callTracer shows control flow — the tree of calls a transaction made, their gas, and any reverts. prestateTracer shows data — the account balances, nonces, code, and storage the transaction read (default mode) or changed (diffMode). Use callTracer to understand what happened, prestateTracer to understand what state was involved.
How do I find which internal call reverted?
Trace with callTracer and walk the tree to the deepest frame carrying an error field. If the contract returned a reason string, the frame also has a decoded revertReason. See Reverts.
How do I get the state needed to replay a transaction locally?
Trace it with prestateTracer in default mode. The result is every account and storage slot the transaction reads, in its pre-execution state — the exact input for a local EVM or an eth_call state override. See Default mode.
Why is my trace request timing out or returning an error?
Tracing is state-heavy. Confirm your node has the debug/trace namespaces enabled and retains the state for the block you’re tracing — recent blocks on a global node, older history on an archive node. Availability and enablement are per-protocol; see Debug and Trace APIs.
Can I use these tracers on chains other than Ethereum?
Yes, on any Geth-based chain that exposes the debug_* namespace (Base, BNB Smart Chain, Optimism, Polygon, Arbitrum via debug_* post-Nitro, Hyperliquid HyperEVM, and more). The Debug and Trace APIs page lists coverage and client differences.
Do I need a custom JavaScript tracer instead?
Only if the built-in tracers don’t capture what you need. callTracer and prestateTracer cover most debugging and state-inspection needs; for bespoke aggregation, write a custom JavaScript tracer.