JSON-RPC Error Codes, Decoded: Retry, Fix, or Accept?
Every developer building on EVM chains eventually stares at {"code":-32000,"message":"..."} at 2am, unsure whether the problem is their code, the node, or the blockchain itself. That distinction is the entire game — because the three cases demand opposite reactions: retry elsewhere, fix your request, or accept the answer. We operate RPC infrastructure across 75+ chains and classify these errors programmatically all day (it's how our failover decides what's safe to retry). Here's the taxonomy, with real payloads captured through our own endpoints while writing this.
The spec-defined codes
JSON-RPC 2.0 reserves five codes, and they mean the same thing on every chain:
| Code | Meaning | Whose problem |
|---|---|---|
| -32700 | Parse error — your JSON is malformed | Yours |
| -32600 | Invalid request — valid JSON, invalid JSON-RPC envelope | Yours |
| -32601 | Method not found | Depends — see below |
| -32602 | Invalid params | Yours |
| -32603 | Internal error | The node's |
Two of these captured live:
// eth_bogusMethod →
{"code":-32601,"message":"the method eth_bogusMethod does not exist/is not available"}
// eth_getBalance("not-an-address") →
{"code":-32602,"message":"Invalid params","data":"invalid string length at line 1 column 16"}
-32601 is the interesting one. It can mean the method doesn't exist anywhere ("eth_bogusMethod") — your bug. Or it can mean this particular node has the method disabled — debug_traceCall on a public endpoint, trace_block on a chain whose upstreams don't run it. Same code, opposite conclusions: one is fix-your-request, the other is try-another-node. Our method-support matrix exists precisely because this ambiguity is otherwise unresolvable without probing.
Code 3: the revert
The most misunderstood "error" isn't an error at all:
// eth_call: transfer more USDC than the sender holds →
{"code":3,"message":"execution reverted: ERC20: transfer amount exceeds balance",
"data":"0x08c379a00000…"}
This is the chain's real answer — the EVM executed your call and the contract said no. The data field is ABI-encoded: 0x08c379a0 is the selector for Error(string), followed by the revert reason (custom errors have their own selectors; your library's decodeErrorResult handles both). Retrying this on another node returns the identical revert, because every node runs the same EVM on the same state. Handle it in your application logic; never in your retry loop. Same story for eth_estimateGas failures on reverting transactions — the estimate fails because the transaction would fail.
The -32000 swamp
Everything from -32000 to -32099 is implementation-defined, and clients dump wildly different situations into it. The messages, not the code, carry the signal:
- "insufficient funds for gas * price + value" — the chain's answer. Fund the account.
- "nonce too low" / "already known" / "replacement transaction underpriced" — nonce lifecycle states; your sequencing, not the node.
- "missing trie node" / "historical state not available" — not a malfunction: you asked a pruned node for old state. The fix is archive routing, not retrying.
- "query returned more than 10000 results" / "block range too large" — you hit a getLogs cap; page your query.
- Rate-limit messages ("too many requests", "exceeded the quota") — a node-capacity statement. Another upstream may have capacity; backoff-and-rotate is correct here.
The retry taxonomy (what our failover actually does)
This is the classification we run in production on every response, and it generalizes to any client:
- Node-capability rejections → safe to try another node. Method missing/disabled, provider-policy refusals ("archive requests require a personal token", "IP or provider is blocked"), rate limits. The node refused to engage with your request, so a different node changes the outcome — and for writes, the transaction was never processed, so a retry can't double-submit.
- Request defects → fix, don't retry. -32602, malformed data, wrong types. Every node rejects these identically.
- Chain answers → accept and handle. Reverts (code 3), insufficient funds, nonce states. Deterministic; retrying is just asking the same question louder.
- Ambient failures → retry with backoff, anywhere. Timeouts, connection resets, HTTP 5xx. The request may or may not have been processed — which is why idempotent design matters for sends.
One transport note: some providers signal rate limits as HTTP 429 (we do, with a Retry-After header — standard HTTP middleware handles it automatically), others as HTTP 200 with a JSON-RPC error body. Robust clients check both layers; code that only reads error.code misses the first kind, and code that only checks HTTP status misses the second.
Debugging shortcut
When an endpoint's errors confuse you, make it explain itself: npx rpc-doctor <url> probes method support, caps, and archive depth, and reports the endpoint's actual error text for each — it's open source and works on any endpoint, including your own node.
And if you'd rather the classification happen server-side: our routing retries category 1 automatically across upstreams and returns categories 2 and 3 to you untouched — that's the failover behavior included on every plan, free tier included.
Related posts
- 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.
- Multicall3 Cheat Sheet: One `eth_call` to Rule Them All
Multicall3 lives at the same address on every EVM chain and turns 100 contract reads into one RPC call. Here's how it actually works, when to use which variant, and the gotchas that bite people in production.
- How `eth_getLogs` Range Caps Bite You in Production
Your indexer works fine until it hits a single dense block range, then everything stops. Here's the cap mechanics across providers, the adaptive chunking pattern that actually works, and the code to do it right.