[SYS]KK.PORTFOLIO / V1.0
Back to Blog

Web3 Development

Ethers.js v6 Smart Contract Integration

Use Ethers.js v6 providers, signers, ABIs, contract reads, transaction writes, confirmations, and chain checks without mixing v5 patterns.

Ketan Kachariya 4 min read

Ethers.js provider and signer connected to an Ethereum contract interface
On This Page

Ethers.js translates JavaScript and TypeScript operations into Ethereum JSON-RPC requests and ABI-encoded contract calls. Version 6 uses native bigint values, BrowserProvider for injected EIP-1193 wallets, and a streamlined Contract API. The most important design decision is not the import syntax; it is choosing the correct connection for each operation. Public reads can use a read-only provider. User-authorized writes need a signer. Keeping those paths separate makes a DApp easier to test and less likely to open unnecessary wallet prompts.

Provider versus signer

A Provider reads blockchain data and broadcasts already signed transactions. JsonRpcProvider connects to an HTTP JSON-RPC endpoint and works well for public reads or server-controlled infrastructure. BrowserProvider wraps a wallet-injected provider in the browser. A Signer represents the ability to request signatures from an account. In a browser wallet, the key remains inside the wallet; getSigner returns an interface that asks the wallet to approve operations. Never attempt to extract the user's private key.

Contract address and ABI form the client boundary

The address selects one deployed contract on one chain. The ABI describes function names, parameters, return values, events, and errors. Both must match the deployment. A correct ABI with the wrong address may call unrelated code; a correct address with a stale ABI may encode a selector the contract does not support. Store addresses by chain ID, derive the frontend ABI from reviewed build artifacts, and fail closed when configuration is missing. For upgradeable systems, document which interface version the frontend expects.

typescript
import { BrowserProvider, Contract, JsonRpcProvider } from "ethers";

const abi = [
  "function totalSupply() view returns (uint256)",
  "function mint(address recipient, uint256 amount)",
];

export async function readSupply(rpcUrl: string, address: string) {
  const provider = new JsonRpcProvider(rpcUrl);
  const contract = new Contract(address, abi, provider);
  return (await contract.totalSupply()) as bigint;
}

export async function mint(address: string, recipient: string, amount: bigint) {
  if (!window.ethereum) throw new Error("Wallet provider unavailable");
  const provider = new BrowserProvider(window.ethereum);
  const network = await provider.getNetwork();
  if (network.chainId !== 1n) throw new Error("Switch to the supported network");

  const signer = await provider.getSigner();
  const contract = new Contract(address, abi, signer);
  const transaction = await contract.mint(recipient, amount);
  return await transaction.wait();
}

Read contract state without a wallet prompt

Read calls execute locally on an RPC node against a selected block. They can still fail because the endpoint is unavailable, the address has no expected code, the call reverts, or the response becomes stale during a reorganization. Use a stable read provider, set sensible request timeouts outside the library where appropriate, and present retry UI. Cache data according to its freshness needs, but include the chain and contract address in cache keys. For historical events, an indexer or backend query is often more dependable than scanning a large block range from every browser.

Send writes and wait for the right confirmation

A contract connected to a signer estimates and submits a transaction after wallet approval. The returned TransactionResponse proves only that a transaction hash exists; it does not prove success. Calling wait() resolves after mining according to the requested confirmation count and returns a receipt. Check the receipt status where the network supports it. Keep the hash available for progress links, handle replacement scenarios, and refetch affected state after confirmation rather than assuming the requested input became the final value.

Handle failures at each layer

A call can fail before the wallet opens, during user approval, while broadcasting, during EVM execution, or while waiting for a receipt. Separate invalid application input from wallet rejection and a contract revert. Ethers errors include machine-readable context, but their shape can vary with the provider, so narrow unknown errors safely. Never show a success toast immediately after calling the contract method. Also avoid retrying a write automatically: a slow response may conceal a transaction that was already submitted.

Identify the chain before constructing the write path

Use provider.getNetwork() and compare the bigint chainId with supported configuration. Do not infer the network from the RPC hostname or from a contract address alone. When chainChanged fires, discard signer-connected Contract instances and pending reads associated with the previous network. If multiple networks are supported, select the address, explorer URL, and confirmation policy from one chain configuration object so they cannot drift independently.

Common v5-to-v6 mistakes

  • Use BrowserProvider instead of the v5 Web3Provider wrapper for injected EIP-1193 providers.
  • Expect bigint values rather than BigNumber for common numeric results.
  • Import utilities such as parseEther directly from ethers instead of through ethers.utils.
  • Await getSigner() and treat the signer as a permissioned write capability.
  • Keep examples within one major-version API rather than combining copied snippets.

Test the adapter at two levels

Unit-test application code against a narrow mocked adapter so rejected signatures, decoded reverts, replacements, and confirmation delays are deterministic. Then run integration tests against a local development node using the compiled ABI and a real deployment. The first layer keeps React tests fast; the second catches encoding, address, event, and signer mistakes that mocks cannot reveal. Include at least one reverted write and assert that no success state is emitted before the receipt.

For production observability, capture method names, chain IDs, timings, and transaction hashes only where the product needs them. Avoid logging signed messages, raw provider payloads, or a user's complete wallet activity. Normalize errors at the adapter boundary but retain a safe diagnostic cause. This gives the interface stable messages while preserving enough context to distinguish configuration errors, provider outages, user rejection, and contract execution failures.

Build a small integration adapter

Centralize provider creation, supported-chain checks, addresses, ABIs, and error normalization in a narrow module. Components can then request a read or a write without reconstructing blockchain plumbing. This makes it possible to mock the adapter in React tests and to update RPC or contract configuration without touching every screen. Ethers.js supplies the protocol-facing primitives; a reliable application still has to define lifecycle, state, and user feedback.

Ethers.jsTypeScriptSmart ContractsEthereumRPC

Building a Web3 Product?

Need help with a DApp, smart contract integration, wallet connection, or production Web3 architecture? Let's discuss the engineering behind your product.

Discuss Your Web3 Project

Keep Reading

React interface connected through a wallet to an Ethereum smart contract

DApp Development

Featured

Build a Web3 DApp with React and Ethereum

A practical architecture guide for connecting a React interface to Ethereum, reading contract state, sending transactions, and designing dependable wallet-aware UI.

Oct 19, 2026 5 min read
Read Article