You send a payment, then refresh its status. Your wallet says it is pending, but the next lookup cannot find it. Reporting failure now could prompt a second payment while the first can still complete.
eth_getTransactionByHash tells you what one node knows about a transaction hash. A null result means "unknown here". It does not mean failed, cancelled or safe to resend.
Use the lookup to distinguish unknown, pending and mined transactions. Then use the sender's nonce to investigate replacements, and the receipt to check execution. Keep the signed bytes and every replacement hash as the submission guide explains.
Separate a missing transaction from a failed lookup
The execution API specification defines a result of null or a transaction object. Its transaction schema describes block context. Geth also returns pooled transactions with blockHash, blockNumber and transactionIndex set to null.
Geth's lookup implementation checks the canonical chain first, then its transaction pool. If both miss while indexing remains incomplete, it returns an error. Otherwise, it returns null. These are Geth behaviors, not guarantees about every provider's routing or storage.
| Answer | Supported conclusion | Next action |
|---|---|---|
| Object with all three block fields set | Included in this node's chain view | Fetch its receipt when you need the execution result |
Object with all three block fields null | Present in this node's pool | Keep waiting; another node may not know it |
null | Unknown to this lookup | Check saved hashes, nonce evidence and endpoint health |
| Mixed or missing block fields | Incomplete or inconsistent response | Reject it and retry |
| JSON-RPC error, HTTP failure or timeout | Lookup failed | Keep the record unresolved and retry with a bounded policy |
Validate hashes locally: require 0x followed by 64 hexadecimal digits, then normalize the digits to lowercase. Do not use permissive server parsing as input validation.

eth_getTransactionByHash answers for one node with three data replies. null means unknown here, not failed or cancelled: check saved hashes and the nonce. An object whose 3 block fields are null means pending here: keep waiting. An object whose 3 block fields are set means mined here: fetch the receipt. An error, a timeout or mixed block fields is a failed read, not a transaction state: keep the record unresolved and retry.
Treat null as unknown here
A node may not have received the transaction. It may have dropped it, accepted a replacement, or lost access to its historical lookup index. A hash subscription and the next HTTP request can also reach different backends.
Check the endpoint's chain ID and head before treating repeated misses as transaction evidence. If you use subscriptions, account for reconnect gaps as the WebSocket recovery guide describes.
Historical lookup needs its own check. Geth's history.transactions setting defaults to about one year of transaction indexing for non-archive nodes. A missing index entry can make a hash lookup fail even when the block remains available.
If you saved the block number, request that block and compare its transactions. Otherwise, use an endpoint with the required transaction history. Historical state access and historical transaction lookup are separate requirements; confirm which one the endpoint retains.
The error transaction indexing is in progress is different from null. Preserve that error as a failed read. Do not convert it into a transaction state.
Pending and mined objects answer different questions
Check all three block fields explicitly. Geth sets block context when the transaction belongs to a block. For a pending transaction, all three fields are null.
A missing field is not null. Reject mixed shapes, and accept transactionIndex: "0x0" as a valid first position in a block.
For a type-2 transaction, Geth's serializer sets pending gasPrice to maxFeePerGas. After inclusion, it uses min(maxPriorityFeePerGas + baseFeePerGas, maxFeePerGas). Read the receipt's effectiveGasPrice and gasUsed when calculating the execution fee.
| You need to know | Read |
|---|---|
| Sender, nonce, destination, value, calldata or block position | Transaction object |
| Success or revert, execution gas used, emitted logs or created contract | Transaction receipt |
| Whether your settlement policy accepts the block | Canonical block checks and your confirmation policy |
Under EIP-658, receipt status is 0x1 for success and 0x0 for failure. A failed transaction still consumes its nonce. A mined transaction object alone cannot tell you whether the payment succeeded.
If you already poll receipts, request the receipt first. Use the transaction lookup when a missing receipt leaves you needing pending-state or sender information. A mined object beside a missing receipt is an unresolved inconsistency; retry before changing the payment's state.
Find the transaction that used the nonce
A nonce is the sender account's sequence number. A replacement uses the same sender and nonce with different signed bytes, producing a different hash. Keep every hash you signed for that nonce, together with the chain ID and submission height.
A fee increase does not guarantee that the replacement reaches every node or becomes canonical. Geth's regular transaction pool defaults to a 10% price bump. Its replacement check requires both fee cap and tip to exceed the previous values and meet the percentage thresholds. This is configurable pool policy; blob transactions use a separate pool.
When the original hash returns null, compare the saved nonce with eth_getTransactionCount on the same endpoint.
| Nonce evidence after a missing hash | What to do |
|---|---|
latest is above the saved nonce | Find what consumed it; the count does not identify the transaction |
latest equals the saved nonce and pending is above it | Investigate pool activity; do not label a specific replacement |
| Counts give no evidence that the nonce is used | Keep the record unknown and check another healthy endpoint |
Geth's pending count includes its pool view. These reads are separate requests, so a block or backend change can make them disagree. An EIP-7702 authorization can also increment an account's nonce without an ordinary transaction from that account.
Find an included replacement by reading full blocks and matching sender plus nonce. The viem receipt waiter uses this mechanism when the requested receipt remains missing. It then fetches the matched transaction's receipt. Its labels describe the changed payload: repriced for unchanged destination, value and input; cancelled for a zero-value self-transfer; otherwise replaced.
For your own recovery worker, scan a bounded range from the saved submission height. Reject missing blocks and incomplete transaction objects. If the range ends without a match, keep the result unresolved. A nonce count alone is not a substitute for finding the transaction.

Four steps recover a transaction whose hash returns null: keep every signed hash with its chain ID and height; treat null as unknown to this node and compare nonce counts; scan full blocks from the saved height for the same sender and nonce; read the receipt of the matched hash. On 4 November 2026, 4 hashes were announced for one sender's nonce 13, and only the third was included, in block 26,338,708. A higher nonce count says the nonce is used, not which hash won.
The newest announcement need not identify the winner. A public Ethereum pending stream captured four hashes for one sender's nonce 13 on 4 November 2026. Follow-up reads on publicnode, dRPC and Tenderly found the third hash in block 26,338,708. The fourth hash returned null. Those dated observations concern those endpoints, not a network-wide propagation guarantee.
Classify a lookup without inventing an outcome
Save this function as lookup.mjs and run it with Node.js 22 or later. Bind the worker to the saved chain ID before calling it. The function checks response identity and block context; it does not settle payments or scan for replacements.
const isHash = (v) => typeof v === 'string' && /^0x[0-9a-fA-F]{64}$/.test(v);
const isAddress = (v) => typeof v === 'string' && /^0x[0-9a-fA-F]{40}$/.test(v);
const isQuantity = (v) => typeof v === 'string' && /^0x(?:0|[1-9a-fA-F][0-9a-fA-F]*)$/.test(v);
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
const same = (a, b) => typeof a === 'string' && typeof b === 'string' && a.toLowerCase() === b.toLowerCase();
let requestId = 0;
async function rpc(url, method, params) {
const id = ++requestId;
const response = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json' },
signal: AbortSignal.timeout(10_000),
body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const body = await response.json();
if (!isObject(body) || body.jsonrpc !== '2.0' || body.id !== id) throw new Error('Invalid RPC envelope');
if (Object.hasOwn(body, 'error')) throw new Error(`RPC error: ${JSON.stringify(body.error)}`);
if (!Object.hasOwn(body, 'result')) throw new Error('Missing RPC result');
return body.result;
}
// saved = { hash, from, nonce } stored before the transaction was submitted.
export async function lookupByHash(url, saved) {
if (!isHash(saved.hash) || !isAddress(saved.from) || !Number.isSafeInteger(saved.nonce) || saved.nonce < 0) {
throw new Error('Saved record needs a 32-byte hash, a sender and a nonnegative integer nonce');
}
const tx = await rpc(url, 'eth_getTransactionByHash', [saved.hash.toLowerCase()]);
if (tx === null) return unknownHere(url, saved);
if (!isObject(tx) || !same(tx.hash, saved.hash) || !same(tx.from, saved.from) ||
!isQuantity(tx.nonce) || BigInt(tx.nonce) !== BigInt(saved.nonce)) {
throw new Error('Mismatched transaction');
}
const nulls = [tx.blockHash, tx.blockNumber, tx.transactionIndex].filter((v) => v === null).length;
if (nulls === 3) return { state: 'pending' };
if (nulls !== 0 || !isHash(tx.blockHash) || !isQuantity(tx.blockNumber) || !isQuantity(tx.transactionIndex)) {
throw new Error('Inconsistent block fields');
}
// Inclusion does not prove successful execution.
return { state: 'mined', blockHash: tx.blockHash, blockNumber: tx.blockNumber, transactionIndex: tx.transactionIndex };
}
async function unknownHere(url, saved) {
// Read both nonce views one after another. They are separate calls, not a snapshot.
const latest = await rpc(url, 'eth_getTransactionCount', [saved.from, 'latest']);
const pending = await rpc(url, 'eth_getTransactionCount', [saved.from, 'pending']);
if (!isQuantity(latest) || !isQuantity(pending)) throw new Error('Invalid nonce reply');
const n = BigInt(saved.nonce);
if (BigInt(latest) > n) return { state: 'unknown-nonce-used', latest, pending };
if (BigInt(latest) === n && BigInt(pending) > n) return { state: 'unknown-nonce-in-pool', latest, pending };
return { state: 'unknown', latest, pending };
}Call it with the hash, sender and nonce you saved before submission:
import { lookupByHash } from './lookup.mjs';
const result = await lookupByHash(process.env.RPC_URL, {
hash: process.env.TX_HASH,
from: process.env.TX_FROM,
nonce: Number(process.env.TX_NONCE),
});
console.log(result);unknown-nonce-used and unknown-nonce-in-pool are investigation hints. Neither identifies a replacement. mined still needs a receipt when execution matters. A thrown error leaves the saved record unresolved.
Give retries a deadline, backoff and an attempt limit. When the limit expires, report an unresolved lookup for investigation. Do not create a new payment because a hash lookup returned null.
Recheck inclusion before settlement
A chain reorganization can remove an included transaction's block. Geth's pool reset attempts to reinsert transactions discarded by a reorganization. Reinsertion does not guarantee acceptance or later inclusion.
Store both blockNumber and blockHash. Before settlement, fetch the canonical block at that number and compare its hash. If it differs, invalidate the previous inclusion and receipt, then investigate again. Apply the confirmation policy described in the safe block guide.
For an Ethereum endpoint, start with Dwellir's transaction lookup reference and receipt reference. Check the unknown, pending, mined and malformed-response paths before connecting the worker to payment handling.




