Back to blog
Jun 03, 2025
4 min read

Web3 Development with ethers.js: A Complete Guide

Master Ethereum blockchain interactions using ethers.js - from basic transactions to complex smart contract interactions

Web3 Development with ethers.js

Ethers.js has become the go-to library for Ethereum blockchain interactions. Let’s explore how to build robust Web3 applications using ethers.js.

Getting Started with ethers.js

  1. Basic Setup
import { ethers } from 'ethers';

// Connect to Ethereum network
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();

// Request account access
async function connectWallet() {
 try {
 await window.ethereum.request({ method: 'eth_requestAccounts' });
 const address = await signer.getAddress();
 console.log('Connected wallet:', address);
 return address;
 } catch (error) {
 console.error('Failed to connect wallet:', error);
 }
}

Smart Contract Interaction

  1. Contract Instance Creation
const contractAddress = "0x123..."; // Your contract address
const contractABI = [
 "function balanceOf(address) view returns (uint)",
 "function transfer(address to, uint amount) returns (bool)",
];

const contract = new ethers.Contract(
 contractAddress,
 contractABI,
 signer
);
  1. Reading Contract Data
async function getTokenBalance(address: string) {
 try {
 const balance = await contract.balanceOf(address);
 // Convert from Wei to Ether
 return ethers.utils.formatEther(balance);
 } catch (error) {
 console.error('Error fetching balance:', error);
 throw error;
 }
}

// Reading events
const filter = contract.filters.Transfer();
contract.on(filter, (from, to, amount) => {
 console.log(`${from} sent ${amount} tokens to ${to}`);
});

Transaction Management

  1. Sending Transactions
async function sendTransaction(to: string, amount: string) {
 try {
 // Convert Ether to Wei
 const amountWei = ethers.utils.parseEther(amount);
 
 // Create transaction
 const tx = await signer.sendTransaction({
 to,
 value: amountWei
 });
 
 // Wait for confirmation
 const receipt = await tx.wait();
 console.log('Transaction confirmed:', receipt.transactionHash);
 return receipt;
 } catch (error) {
 console.error('Transaction failed:', error);
 throw error;
 }
}
  1. Gas Estimation
async function estimateGas(to: string, amount: string) {
 const amountWei = ethers.utils.parseEther(amount);
 
 // Estimate gas limit
 const gasLimit = await signer.estimateGas({
 to,
 value: amountWei
 });
 
 // Get current gas price
 const gasPrice = await provider.getGasPrice();
 
 // Calculate total gas cost
 const gasCost = gasLimit.mul(gasPrice);
 return ethers.utils.formatEther(gasCost);
}

Advanced Contract Interactions

  1. Contract Deployment
async function deployContract(contractBytecode: string, contractABI: any[]) {
 const factory = new ethers.ContractFactory(
 contractABI,
 contractBytecode,
 signer
 );
 
 const contract = await factory.deploy();
 await contract.deployed();
 
 console.log('Contract deployed to:', contract.address);
 return contract;
}
  1. Batch Transactions
async function batchTransfer(recipients: string[], amounts: string[]) {
 // Create a multi-call contract instance
 const multicall = new ethers.Contract(
 MULTICALL_ADDRESS,
 MULTICALL_ABI,
 signer
 );
 
 const calls = recipients.map((recipient, index) => ({
 target: TOKEN_ADDRESS,
 callData: contract.interface.encodeFunctionData('transfer', [
 recipient,
 ethers.utils.parseEther(amounts[index])
 ])
 }));
 
 const tx = await multicall.aggregate(calls);
 return await tx.wait();
}

Error Handling and Security

  1. Transaction Error Handling
async function safeContractCall(
 method: string,
 params: any[]
): Promise<any> {
 try {
 const tx = await contract[method](...params);
 const receipt = await tx.wait();
 
 if (receipt.status === 0) {
 throw new Error('Transaction failed');
 }
 
 return receipt;
 } catch (error: any) {
 if (error.code === 'INSUFFICIENT_FUNDS') {
 throw new Error('Not enough ETH for transaction');
 }
 if (error.code === 'UNPREDICTABLE_GAS_LIMIT') {
 throw new Error('Contract call will fail');
 }
 throw error;
 }
}
  1. Signature Verification
async function verifySignature(
 message: string,
 signature: string,
 address: string
): Promise<boolean> {
 try {
 const signerAddr = ethers.utils.verifyMessage(message, signature);
 return signerAddr.toLowerCase() === address.toLowerCase();
 } catch {
 return false;
 }
}

Event Monitoring

  1. Real-time Event Tracking
function watchContractEvents() {
 contract.on('Transfer', (from, to, amount, event) => {
 console.log({
 from,
 to,
 amount: ethers.utils.formatEther(amount),
 blockNumber: event.blockNumber,
 transactionHash: event.transactionHash
 });
 });
}
  1. Historical Event Query
async function getHistoricalEvents(
 fromBlock: number,
 toBlock: number
) {
 const events = await contract.queryFilter(
 contract.filters.Transfer(),
 fromBlock,
 toBlock
 );
 
 return events.map(event => ({
 from: event.args?.from,
 to: event.args?.to,
 amount: ethers.utils.formatEther(event.args?.amount),
 blockNumber: event.blockNumber,
 transactionHash: event.transactionHash
 }));
}

Best Practices

  1. Provider Management
  • Always handle network changes
  • Implement proper fallback providers
  • Cache provider connections
  1. Transaction Safety
  • Always estimate gas before sending
  • Implement proper nonce management
  • Handle transaction replacements
  1. Error Recovery
  • Implement proper retry mechanisms
  • Handle RPC errors gracefully
  • Monitor transaction status

Resources