JSON-RPC Error Codes, Decoded: Retry, Fix, or Accept?

August 28, 2026 · 4 min read · #json-rpc #errors #tutorial #debugging

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:

  1. 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.
  2. Request defects → fix, don't retry. -32602, malformed data, wrong types. Every node rejects these identically.
  3. Chain answers → accept and handle. Reverts (code 3), insufficient funds, nonce states. Deterministic; retrying is just asking the same question louder.
  4. 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

Try SwiftNodes free — multi-chain RPC across 75+ networks, flat-rate pricing, pay by card or crypto, no KYC. Get an API key in 30 seconds →