Skip to content

ethers.js: Modern Ethereum JavaScript Library Guide

DodaTech Updated 2026-06-22 8 min read

In this tutorial, you'll learn ethers.js including providers, signers, contract interactions, event filtering, and utilities for building production dApps. Why it matters: ethers.js is the most popular Ethereum JavaScript library with a clean, TypeScript-first API, comprehensive documentation, and significantly smaller bundle size than Web3.js. By the end, you'll build a complete dApp frontend using ethers.js.

ethers.js is a lightweight, comprehensive JavaScript library for interacting with the Ethereum blockchain, designed with safety, simplicity, and TypeScript support as core principles.

Setting Up ethers.js

Install ethers.js and connect to an Ethereum network through a provider.

npm init -y
npm install ethers
const { ethers } = require("ethers");

// Connect to Ethereum mainnet via a JSON-RPC provider
const provider = new ethers.JsonRpcProvider("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY");

// Or use the default provider (automatically connects to Ethereum)
// const provider = ethers.getDefaultProvider();

async function getNetworkInfo() {
  const network = await provider.getNetwork();
  console.log("Network:", network.name);
  console.log("Chain ID:", network.chainId);
  
  const blockNumber = await provider.getBlockNumber();
  console.log("Latest block:", blockNumber);
  
  const gasPrice = await provider.getFeeData();
  console.log("Gas price:", ethers.formatUnits(gasPrice.gasPrice, "gwei"), "gwei");
}

getNetworkInfo();
// Expected output:
// Network: mainnet
// Chain ID: 1
// Latest block: 19472345
// Gas price: 12.5 gwei
flowchart TD
  A[dApp Frontend] --> B[ethers.js]
  B --> C[Provider
Read-only connection] B --> D[Signer
Can sign transactions] B --> E[Contract
ABI + Address] C --> F[Ethereum Node
RPC/WebSocket] D --> F D --> G[Wallet
Private Key / Mnemonic] E --> H[call() - Read state] E --> I[Contract Runner
connect(signer) - Write] I --> J[sendTransaction()]

Using Providers

Providers give read-only access to the blockchain. ethers.js supports multiple provider types.

Provider Type Connection Use Case
JsonRpcProvider HTTP URL Direct node access
WebSocketProvider WebSocket URL Real-time subscriptions
InfuraProvider Project ID Infura managed nodes
AlchemyProvider API key Alchemy managed nodes
CloudflareProvider None Cloudflare-Cloudflare-Cloudflare-Cloudflare-Ethereum Gateway
const { ethers } = require("ethers");

// Multiple ways to create providers
const jsonRpcProvider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_ID");

// WebSocket for subscriptions
const wsProvider = new ethers.WebSocketProvider("wss://mainnet.infura.io/ws/v3/YOUR_ID");

// Alchemy provider
const alchemyProvider = new ethers.AlchemyProvider("mainnet", "YOUR_API_KEY");

async function providerComparison() {
  // Get block with transactions
  const block = await jsonRpcProvider.getBlock(19472345, true);
  console.log("Block:", block.number);
  console.log("Transactions:", block.transactions.length);
  console.log("Timestamp:", new Date(block.timestamp * 1000));
  
  // Get account balance
  const vitalikAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
  const balance = await jsonRpcProvider.getBalance(vitalikAddress);
  console.log("Vitalik ETH:", ethers.formatEther(balance));
  
  // Get code at address (check if contract)
  const code = await jsonRpcProvider.getCode(vitalikAddress);
  console.log("Is contract:", code !== "0x");
  
  // Get storage at a slot
  const storage = await jsonRpcProvider.getStorage("0x...", 0);
}

providerComparison();
// Expected output:
// Block: 19472345
// Transactions: 178
// Timestamp: Tue Jun 22 2026 ...
// Vitalik ETH: 2345.67
// Is contract: false

Working with Signers and Wallets

Signers can sign transactions and messages. Wallets manage private keys.

const { ethers } = require("ethers");

// Create a wallet from private key
const privateKey = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d";
const wallet = new ethers.Wallet(privateKey);
console.log("Wallet address:", wallet.address);

// Create wallet from mnemonic
const mnemonic = "test test test test test test test test test test test junk";
const mnemonicWallet = ethers.Wallet.fromPhrase(mnemonic);
console.log("Mnemonic wallet:", mnemonicWallet.address);

// Connect wallet to a provider
const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");
const signer = wallet.connect(provider);

async function signerActions() {
  // Sign a message
  const message = "Hello, Ethereum!";
  const signature = await signer.signMessage(message);
  console.log("Signature:", signature);
  
  // Verify signature
  const recovered = ethers.verifyMessage(message, signature);
  console.log("Recovered address:", recovered);
  console.log("Match:", recovered === wallet.address);
  
  // Send a transaction
  const tx = await signer.sendTransaction({
    to: "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18",
    value: ethers.parseEther("0.1"),
  });
  
  console.log("Transaction hash:", tx.hash);
  console.log("Transaction:", tx);
  
  // Wait for confirmation
  const receipt = await tx.wait();
  console.log("Confirmed in block:", receipt.blockNumber);
  console.log("Gas used:", receipt.gasUsed.toString());
}

signerActions();
// Expected output:
// Signature: 0xabc123...
// Recovered address: 0x90F8...
// Match: true
// Transaction hash: 0x7a3f...
// Confirmed in block: 3
// Gas used: 21000

Interacting with Contracts

Use the Contract class to interact with deployed contracts by providing the address and ABI.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract Counter {
    uint256 public count;
    
    event CountChanged(uint256 newCount);
    
    function increment() external {
        count += 1;
        emit CountChanged(count);
    }
    
    function decrement() external {
        require(count > 0, "Cannot go below zero");
        count -= 1;
        emit CountChanged(count);
    }
    
    function getCount() external view returns (uint256) {
        return count;
    }
}
const { ethers } = require("ethers");

const counterABI = [
  "function count() view returns (uint256)",
  "function increment()",
  "function decrement()",
  "function getCount() view returns (uint256)",
  "event CountChanged(uint256 newCount)",
];

const counterAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");
const signer = new ethers.Wallet("0x...privatekey...", provider);

// Create read-only contract (uses provider)
const counterContract = new ethers.Contract(counterAddress, counterABI, provider);

// Create writable contract (uses signer)
const counterWithSigner = new ethers.Contract(counterAddress, counterABI, signer);

async function interactWithContract() {
  // Read (call - free)
  const count = await counterContract.getCount();
  console.log("Current count:", count);
  
  // Write (send - costs gas)
  const tx = await counterWithSigner.increment();
  const receipt = await tx.wait();
  
  // Read again
  const newCount = await counterContract.getCount();
  console.log("New count:", newCount);
  
  // Listen for events
  counterContract.on("CountChanged", (newCount, event) => {
    console.log("Count changed to:", newCount);
    console.log("Block:", event.log.blockNumber);
  });
  
  // Get past events
  const filter = counterContract.filters.CountChanged();
  const events = await counterContract.queryFilter(filter, 0, "latest");
  console.log("Past CountChanged events:", events.length);
}

interactWithContract();
// Expected output:
// Current count: 0
// New count: 1
// Count changed to: 1
// Block: 5
// Past CountChanged events: 1

Event Filtering and Subscriptions

ethers.js provides powerful event filtering with typed parameters.

const { ethers } = require("ethers");

const abi = [
  "event Transfer(address indexed from, address indexed to, uint256 value)",
  "function transfer(address to, uint256 value) returns (bool)",
  "function balanceOf(address) view returns (uint256)",
];

const tokenAddress = "0x...";
const provider = new ethers.WebSocketProvider("wss://mainnet.infura.io/ws/v3/YOUR_ID");
const contract = new ethers.Contract(tokenAddress, abi, provider);

async function eventExamples() {
  // Filter by specific addresses
  const myAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
  
  // Filter: Transfer FROM myAddress
  const filterFrom = contract.filters.Transfer(myAddress);
  
  // Filter: Transfer TO myAddress
  const filterTo = contract.filters.Transfer(null, myAddress);
  
  // Filter: Transfer between two addresses
  const filterBoth = contract.filters.Transfer(myAddress, "0x...");
  
  // Subscribe to all Transfer events
  contract.on("Transfer", (from, to, value, event) => {
    console.log(`${from} -> ${to}: ${ethers.formatEther(value)} tokens`);
    console.log("Block:", event.log.blockNumber);
    console.log("Tx:", event.log.transactionHash);
  });
  
  // Query past events
  const pastToEvents = await contract.queryFilter(filterTo, 19470000, 19472345);
  console.log(`Found ${pastToEvents.length} incoming transfers`);
  
  // Unsubscribe
  contract.off("Transfer");
}

eventExamples();
// Expected output:
// 0xd8dA... -> 0x742d...: 1000 tokens
// Block: 19472345
// Tx: 0xabc123...
// Found 5 incoming transfers

Utility Functions

ethers.js includes extensive utility functions for data formatting and parsing.

const { ethers } = require("ethers");

// Unit conversion
console.log(ethers.parseEther("1.0"));
// 1000000000000000000n

console.log(ethers.formatEther("1000000000000000000"));
// "1.0"

console.log(ethers.parseUnits("50.5", 6));
// 50500000n

console.log(ethers.formatUnits("50500000", 6));
// "50.5"

// Address utilities
const address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
console.log(ethers.getAddress(address));
// 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 (checksummed)

console.log(ethers.isAddress(address));
// true

// Hashing
console.log(ethers.id("hello"));
// 0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8

console.log(ethers.solidityPackedKeccak256(["address", "uint256"], [address, 100]));
// 0x... (packed hash)

// Data encoding
const encoded = ethers.AbiCoder.defaultAbiCoder().encode(
  ["address", "uint256"],
  [address, 42]
);
console.log("Encoded:", encoded);
// Encoded: 0x0000... (padded hex)

const decoded = ethers.Abcoder.defaultAbiCoder().decode(
  ["address", "uint256"],
  encoded
);
console.log("Decoded:", decoded[0], decoded[1].toString());
// Decoded: 0xd8dA... 42

Common Errors and Misunderstandings

1. Provider vs Signer Confusion

Providers can only read data. Signers can write. Reading requires only a provider. Writing requires a signer connected to a provider.

2. Not Waiting for Confirmations

Transactions are pending until confirmed. Always await tx.wait() before checking state changes.

3. Using Wrong Network

Connecting to mainnet when you meant to use testnet causes deployment and balance issues. Always verify provider.getNetwork().

4. Forgetting to Connect Wallet

A wallet created with new ethers.Wallet(key) is not connected to a provider. Use wallet.connect(provider) to enable network interaction.

5. Event Listener Memory

Adding event listeners without removing them causes memory leaks. Always call contract.off("EventName") in cleanup.

Practice Questions

  1. What is the difference between a Provider and a Signer in ethers.js? A Provider gives read-only access to the blockchain (querying balances, blocks, state). A Signer can authorize transactions and sign messages. A Signer extends a Provider.

  2. How do you format ETH amounts in ethers.js? Use ethers.parseEther("1.5") to convert ETH string to BigInt (wei), and ethers.formatEther("1500000000000000000") to convert wei to ETH string.

  3. What does tx.wait() return? It returns a TransactionReceipt containing the block number, gas used, logs (events), status (1 for success, 0 for failure), and effective gas price.

  4. How do you filter events by address? Use contract.filters.EventName(parameter1, parameter2). Pass the specific address for indexed parameters, or null to match any value.

  5. What is the difference between ethers.id() and ethers.solidityPackedKeccak256()? ethers.id() hashes a UTF-8 string (like keccak256 of string bytes). solidityPackedKeccak256() hashes packed Solidity types, matching Solidity's keccak256(abi.encodePacked(...)).

Challenge

Build a TypeScript application using ethers.js that monitors multiple ERC-20 token balances for a given wallet address, detects large transfers (over $10,000 equivalent), and writes alerts to a JSON log file with price data fetched from a DEX.

Real-World Task

Create a React dashboard using ethers.js that connects to MetaMask, displays the user's ETH and ERC-20 balances, allows token transfers with gas estimation, shows transaction history with status indicators, and listens for real-time balance updates via WebSocket provider.

Frequently Asked Questions

Is ethers.js better than Web3.js?

ethers.js is generally preferred for modern dApps due to its TypeScript-first design, smaller bundle size (88KB vs 200KB+), cleaner API, better documentation, and built-in security features like address checksumming and ENS resolution.

Can ethers.js connect to any EVM blockchain?

Yes. ethers.js works with any EVM-compatible chain (Polygon, Arbitrum, Optimism, BNB Chain, Avalanche). Configure the provider URL and chain ID for your target network. The API remains identical across chains.

Does ethers.js support ENS names?

Yes. ethers.js has built-in ENS support. Use provider.resolveName("vitalik.eth") to get an address and provider.lookupAddress("0x...") to get the ENS name. This works automatically when you pass an ENS name to any function.

Next Steps

After mastering ethers.js, build full-stack dApps with React and MetaMask integration, then explore advanced topics like DeFi protocol interaction and smart contract security.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro