Did It Actually Succeed? Reading eth_getTransactionReceipt
Here's a bug that reaches production more often than it should: code sends a transaction, waits for it to be mined, sees it in a block, and reports "success." But the transaction reverted — it failed, it changed nothing, and the user still paid for the gas. "Mined" and "succeeded" are two different facts, and the only place that tells you the difference is the transaction receipt.
Once you've estimated gas and set your fees and the transaction has left the mempool into a block, the receipt is how you close the loop. Let's read one properly.
Getting the receipt
eth_getTransactionReceipt takes a transaction hash and returns the receipt — or null if the transaction isn't mined yet (or the node has never seen it):
curl -s -X POST https://rpc.swiftnodes.io/rpc/eth?key=YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_getTransactionReceipt",
"params":["0xYourTxHash"],"id":1}'
That null is meaningful. A receipt only exists once a transaction is included in a block — so null means one of: still pending in the mempool, dropped/replaced, or never broadcast. Don't treat null as failure; treat it as "not mined yet," and keep waiting (or resubmit if it's been dropped).
The one field everyone should check: status
Since the Byzantium upgrade (2017), every receipt carries a status:
status |
Meaning |
|---|---|
0x1 |
Success — the transaction executed to completion |
0x0 |
Failed / reverted — it was included in a block but did nothing |
This is the field that catches the bug in the intro. A transaction with status: 0x0 is a real, permanent block entry — it consumed gas up to the point it reverted, the sender paid for that gas, and the nonce was used up. It just didn't accomplish anything (a require failed, an assertion tripped, it ran out of gas). So the rule is:
Mined ≠ succeeded. Always check
status === 1before you tell a user their action worked.
Libraries make this easy to forget because tx.wait() resolves for both successful and reverted transactions — in ethers it will throw on a revert, but only if you actually await .wait() and don't swallow it. Check the status explicitly.
Reading the event logs
The receipt's logs array is the events your transaction emitted — this is how you extract "what happened" (a transfer amount, a new token ID, a pool's swap output). Each log entry has:
address— the contract that emitted ittopics[]— indexed fields;topics[0]is the event signature hash (keccak256("Transfer(address,address,uint256)")), and the rest are indexed parametersdata— the non-indexed parameters, ABI-encoded
You decode these with the contract ABI. In practice your library does it for you — but knowing the shape is what lets you filter a receipt's logs for the one event you care about, or debug why a log isn't showing up (usually because the field you want wasn't declared indexed). This is the same log structure you'd stream live over eth_subscribe; the receipt just gives you the authoritative, post-inclusion copy.
The gas and price fields
The receipt also tells you what the transaction actually cost — the ground truth that your earlier estimate was only predicting:
gasUsed— the gas units this transaction actually consumed (compare against the limit you set; a big gap means you over-buffered, which is fine — unused gas was refunded).cumulativeGasUsed— total gas used by this transaction plus every transaction before it in the block. Useful for block-level accounting, not per-tx cost.effectiveGasPrice— the actual per-gas price paid under EIP-1559 (baseFee + tip). Multiply bygasUsedfor the real fee:
fee paid (wei) = gasUsed × effectiveGasPrice
That closes the loop on gas estimation: you buffered the limit, set fee ceilings, and the receipt reports what was really spent.
One more field worth knowing: contractAddress is null for a normal transaction, but on a contract-creation transaction it holds the address of the newly deployed contract — the canonical way to learn where your deploy landed.
Don't trust one confirmation
A receipt appears the moment the transaction is included — at one confirmation. But a block one deep can still be reorged out, taking your transaction's receipt (and its blockNumber, or the whole receipt) with it. For anything that matters — crediting a deposit, releasing goods, triggering downstream actions — wait for N confirmations appropriate to the chain, or anchor against the finalized block tag.
The practical shape in each library:
// viem — wait for the receipt, then require 2 confirmations, then check status
const receipt = await publicClient.waitForTransactionReceipt({
hash,
confirmations: 2,
});
if (receipt.status !== "success") throw new Error("transaction reverted");
// ethers v6 — tx.wait(n) resolves after n confirmations and THROWS on revert
const receipt = await tx.wait(2);
// reaching here means success; receipt.status === 1
# web3.py — blocks until mined, then check status yourself
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
assert receipt["status"] == 1, "transaction reverted"
Set the confirmation count to the chain, not a habit: a couple of blocks on a slow L1, more on a chain with frequent reorgs, and remember L2 "confirmations" mean soft-confirmed-by-sequencer until the batch settles (see Soft vs Hard Finality).
Why receipts can be heavy
A receipt with a large logs array (a busy DeFi transaction can emit dozens) is a bigger payload than the transaction itself, and fetching receipts for every transaction in a block one-by-one is a lot of round-trips. That's exactly the pain that eth_getBlockReceipts solves — all receipts for a block in a single call — which is worth reaching for when you're indexing rather than checking one transaction. (A dedicated post on that is coming.)
The short version
The receipt is the source of truth for a sent transaction. eth_getTransactionReceipt returns null until the transaction is mined — then the status field tells you success (0x1) vs revert (0x0), and mined is not the same as succeeded: a reverted transaction still sits in a block and still costs gas. Read logs for what happened, gasUsed × effectiveGasPrice for what it cost, and contractAddress for deploys. A receipt shows up at one confirmation but can vanish in a reorg — wait for enough confirmations before you act on it.
All of this needs an endpoint that returns receipts reliably and quickly, with the logs intact. A flat-rate Ethereum RPC endpoint gives you eth_getTransactionReceipt, eth_getBlockReceipts, and the full log surface across dozens of chains under one key. Grab a free key and point your app at:
https://rpc.swiftnodes.io/rpc/eth?key=YOUR_API_KEY
Related posts
- Estimating Gas Right: eth_estimateGas, EIP-1559, and Buffers
Half of failed transactions are gas mistakes: an 'out of gas' revert or a fee too low to ever get mined. Here's how eth_estimateGas actually works, how EIP-1559 fees are built, why you buffer the gas limit, and how to put it all together without overpaying.
- Handling Chain Reorgs in Your Indexer
Your indexer works perfectly in testing, then a reorg silently corrupts your database with transactions that no longer exist. Here's how reorgs break indexers and two proven patterns to handle them — confirmation lag and reorg detection with rollback.
- The Real Cost of `debug_traceTransaction` (and When to Use It)
Tracing a transaction costs 20-30× more than reading its receipt. Sometimes that's worth it; usually it isn't. Here's what trace actually tells you, the four tracer modes, and when a cheaper RPC method gets the same answer.