You call a contract method with ethers or viem, and instead of a number or an address you get a wall of text ending in could not decode result data (value="0x"). The stack trace points at your own code, but the actual fault is a few hops upstream: the node handed back zero bytes, and the ABI decoder had nothing to decode. Four situations cause this, and they are not equally likely.
TL;DR
value="0x" means the RPC node returned empty data instead of your function’s return value. In order of frequency: you’re reading a proxy through the wrong address or ABI, the function doesn’t exist on the deployed contract, the contract is paused or its bytecode is gone, or you’re on the wrong chain or a lagging RPC endpoint. Check eth_getCode first, then the EIP-1967 implementation slot.
Why the decoder has nothing to decode
eth_call is a request/response round trip: your calldata goes in, raw bytes come back. Ethers and viem’s job is to take those bytes and slice them into the types your ABI says the function returns, an address here, a uint256 there. When the node returns literally zero bytes, 0x, there is nothing to slice. The library doesn’t throw a revert error (which usually carries a reason string or a custom error selector), it throws a decode error, because from its point of view the call “succeeded” and simply produced no data.
That distinction matters for debugging. This error means the JSON-RPC call itself didn’t fail, so retrying it with the same address, same ABI, and same call won’t help. Something about what you’re calling, or where you’re calling it, is off.
Cause 1: you’re reading a proxy that hasn’t been resolved
Most production EVM contracts you’ll integrate against today sit behind an upgradeable proxy, usually the EIP-1967 standard slot layout. The proxy holds no business logic itself; it delegatecalls into a separate implementation contract and forwards whatever comes back. That only works correctly if your code knows, for a given address, whether it’s a proxy and which implementation it currently points at, and gets that answer before you build your Contract instance or ABI binding.
This is the single most common cause of the error, because it’s easy to get half right: point at the wrong one of the two addresses (the raw implementation contract instead of the proxy, most often), and any function that reads real state comes back empty. The implementation contract’s own storage is typically blank because the actual state lives in the proxy’s storage slots, and some teams additionally guard implementations against direct calls (via _disableInitializers() in the constructor), which can make reads against the implementation address fail outright.
To diagnose it, confirm the address has code, then read the EIP-1967 implementation slot yourself:
import { createPublicClient, http } from "viem";import { mainnet } from "viem/chains";
const client = createPublicClient({ chain: mainnet, transport: http() });
// bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)const EIP1967_IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
const code = await client.getCode({ address: targetAddress });if (!code || code === "0x") throw new Error("no contract deployed at this address");
const raw = await client.getStorageAt({ address: targetAddress, slot: EIP1967_IMPLEMENTATION_SLOT,});
// the address occupies the last 20 bytes (40 hex chars) of the 32-byte slotconst implementation = raw && raw !== `0x${"0".repeat(64)}` ? `0x${raw.slice(-40)}` : null;The equivalent in ethers v6 uses provider.getStorage(address, position) (the current name for what was getStorageAt in v5):
const raw = await provider.getStorage(targetAddress, EIP1967_IMPLEMENTATION_SLOT);const implementation = ethers.getAddress(`0x${raw.slice(-40)}`);If raw comes back all zeros, the address isn’t an EIP-1967 proxy (or hasn’t been initialized), which tells you your implementation ABI is being tested against the wrong bytecode entirely. Once you have the real implementation address, use it only to fetch the correct ABI, keep calling through the proxy’s address.
Cause 2: the function doesn’t exist on this contract
This is a plain wrong-ABI problem: an ERC-20 ABI applied to a contract that isn’t a token, or an ABI pulled from a different version of the same protocol whose function signatures moved on. When the function selector in your calldata doesn’t match anything the deployed bytecode understands, and the contract has no fallback that handles it gracefully, the call reverts. Depending on the RPC provider, that revert can surface without a reason string, and some providers flatten “reverted with no data” into the same empty response as “succeeded with no data.” Ethers and viem can’t tell those two apart from the raw bytes alone, so you get the same decode error either way.
To diagnose it, first rule out cause 1 or 3 with eth_getCode, then call a function you’re certain is correct for that contract, name() or symbol() on anything claiming to be a token, as a sanity probe. If the probe also comes back empty, look at causes 3 and 4 instead. If the probe works but your target function doesn’t, re-fetch the verified source for the exact address you’re calling. Don’t reuse an ABI from a sibling contract in the same protocol family; upgrades and per-market deployments drift.
Cause 3: the contract is paused, or its bytecode is gone
Two distinct failures share this bucket. The first is a pause flag: many protocols guard state-changing functions, and occasionally read functions too, behind a require(!paused) check. From the decoder’s point of view, that revert looks identical to cause 2, so check the contract’s own paused() getter if it exposes one.
The second is more permanent: if a contract executes SELFDESTRUCT, eth_getCode for that address returns 0x from that point forward, and every call to it, regardless of ABI, comes back empty. EIP-6780, active since the March 2024 Dencun upgrade, narrowed this considerably: SELFDESTRUCT now only actually removes a contract’s code when it’s called in the same transaction that deployed it. Contracts destroyed before Dencun stay destroyed under the old rules, which is exactly what happened in 2017 when a user accidentally triggered self-destruct on a shared Parity multisig library contract, permanently bricking every wallet that delegated its logic to that address.
Diagnosis here is the simplest of the four: eth_getCode at the exact address and block you’re calling. If it’s 0x, no ABI decodes a result, because there’s no contract left to execute your call.
Cause 4: wrong chain, or a lagging RPC endpoint
Address collisions across chains are routine, deterministic deployers (CREATE2 factories, vanity addresses) put the same address on Ethereum, Base, and BNB Chain on purpose, and copy-pasting an address from the wrong network’s block explorer is an easy mistake. A call to a real, well-formed address on the wrong chain just hits an empty account: no code deployed there, and the node returns 0x without complaint, because from the EVM’s perspective that’s a correct answer.
A sneakier variant of the same failure shows up on public, load-balanced RPC endpoints. Your requests get routed across many backend nodes, and if the specific node answering a given request is still syncing or serving a stale snapshot, it can report empty state for a contract that objectively exists elsewhere on the same chain.
Confirm the chain ID matches what you expect (eth_chainId, or your client’s own chain.id), and cross-check the address on that chain’s block explorer before touching your own code again.
How evmquery skips the proxy problem entirely
Working through cause 1 by hand, fetch bytecode, read a storage slot, extract an address, fetch a second ABI, is exactly the bookkeeping evmquery’s contract resolution removes. describe_schema and execute_query resolve the correct implementation ABI against a target address server-side, following EIP-1967 and diamond proxies, before your query ever runs.
Here’s Aave V3’s Pool contract on Ethereum, a proxy in production, queried live while writing this post:
describe_schema({ chain: "evm_ethereum", schema: { contracts: { aave_pool: "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" } }})
→ aave_pool (0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2) resolution: verified dispatches via eip1967 to 0x728a138A4823392C2EFA55e028d434F526fE03CF getReservesCount() -> sol_int -- sourcify, reads from 0x728a...E03CF ...evmquery reports the resolution up front (dispatches via eip1967 to ...), and every method it lists is already bound to the implementation contract. Running a real query against the proxy address returns real data, no manual unwinding required:
execute_query({ chain: "evm_ethereum", schema: { contracts: { aave_pool: "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" } }, expression: "aave_pool.getReservesCount()"})
→ Result: 67 (sol_int) Block: 25690598 | Calls: 1 | Rounds: 1Over REST, the same query looks like this. The shape below follows the documented request/response format in evmquery’s API reference; the query itself (contract, chain, and expression) is the exact one validated live above.
curl -s -X POST https://api.evmquery.com/api/v1/query \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "aave_pool": { "address": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" } } }, "expression": "aave_pool.getReservesCount()" }' | python3 -m json.tool
# {# "result": { "value": 67, "type": "sol_int" },# "meta": { "blockNumber": ... }# }If you’re building the kind of internal tooling or automation that needs to read contract state without carrying proxy-resolution logic yourself, the developer-focused overview of evmquery covers how this resolution fits into a larger integration.
Next steps
- If you’re batching several reads once your ABI resolution is correct, the Multicall3 guide covers the same proxy-ABI mismatch pitfall in a batched-call context.
- Debugging this class of error outside a JS stack? Reading EVM contract data from Python walks through the same resolution problem with web3.py.
- See pricing for evmquery’s free tier if you want to validate a query before wiring it into your own code.



