Swapping ethers for viem
I moved a small dapp backend from ethers v6 to viem, mostly for the typing. Read a contract and the return value has the right shape, derived from the ABI, with no generics to write and no typechain step in the build.
One condition, and it’s the thing people miss on day one:
const abi = [ { name: 'balanceOf', type: 'function', stateMutability: 'view', inputs: [{ name: 'owner', type: 'address' }], outputs: [{ name: '', type: 'uint256' }] },] as const;
const balance = await client.readContract({ address: '0x…', abi, functionName: 'balanceOf', args: [owner],});Without as const, TypeScript widens the ABI to string and every bit of inference collapses. functionName stops autocompleting, args stops being checked, and balance comes back as something useless. If viem feels no better than ethers, check for a missing as const before anything else.
bigint is most of the migration
viem uses native bigint where ethers used BigNumber. Arithmetic gets nicer, since a * b / c just works instead of a.mul(b).div(c).
Serialization gets worse. JSON.stringify throws on a bigint rather than coercing it, so every API response carrying a token amount needs a replacer or an explicit conversion. Mine failed in a route handler, not at the call site, well after the part I was actually migrating.
Two smaller ones
Addresses are typed as `0x${string}`, so a plain string from your database won’t typecheck. Run it through getAddress(), which checksums and narrows in one step.
Revert reasons are nested. Catching the error isn’t enough, you have to walk it:
const revert = error.walk((e) => e instanceof ContractFunctionRevertedError);The clients split too. PublicClient for reads and WalletClient for signing, rather than one provider that quietly does both. It’s more explicit, and it makes it obvious in review when a code path suddenly needs a key.
Worth the afternoon. Budget it for the bigint edges rather than the API surface, because the API surface maps over almost line for line.
