How to Connect to Ethereum with ethers.js v6: Complete Guide
ethers.js is the most widely used JavaScript library for interacting with Ethereum and EVM-compatible blockchains. This guide covers everything you need to go from zero to a fully connected dApp using ethers.js v6, the current major version.
If you are coming from v5, the import style and some provider names have changed. This guide uses v6 throughout.
Prerequisites
- Node.js 18+ installed
- An RPC endpoint (HTTP and WebSocket URLs)
- Basic JavaScript/TypeScript knowledge
Install ethers.js v6
npm install ethers
Verify you have v6:
npm list ethers
# Should show ethers@6.x.x
Connecting with an HTTP Provider
The JsonRpcProvider connects to any Ethereum node over HTTP. Use this for all read operations and transaction submission.
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider(
'https://eu.endpoints.matrixed.link/rpc/ethereum?auth=YOUR_KEY'
);
// Verify connection
const blockNumber = await provider.getBlockNumber();
console.log('Current block:', blockNumber);
That’s it. You are connected. Every read operation on Ethereum now goes through this provider.
Reading a Wallet Balance
const address = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; // vitalik.eth
const balance = await provider.getBalance(address);
console.log('Balance (wei):', balance.toString());
console.log('Balance (ETH):', ethers.formatEther(balance));
// Balance (ETH): 1234.567890123456789
Getting the Current Gas Price
const feeData = await provider.getFeeData();
console.log('Max fee per gas:', ethers.formatUnits(feeData.maxFeePerGas, 'gwei'), 'gwei');
console.log('Max priority fee:', ethers.formatUnits(feeData.maxPriorityFeePerGas, 'gwei'), 'gwei');
Getting Block Data
const block = await provider.getBlock('latest');
console.log('Block number:', block.number);
console.log('Block timestamp:', new Date(block.timestamp * 1000).toISOString());
console.log('Gas used:', block.gasUsed.toString());
Reading Smart Contract State
To interact with a contract, you need the contract address and its ABI (Application Binary Interface).
// Minimal ERC-20 ABI — just the methods we need
const ERC20_ABI = [
'function name() view returns (string)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
'function totalSupply() view returns (uint256)',
'function balanceOf(address owner) view returns (uint256)',
];
const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const usdcContract = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, provider);
// Read contract state — these are eth_call under the hood
const name = await usdcContract.name(); // "USD Coin"
const symbol = await usdcContract.symbol(); // "USDC"
const decimals = await usdcContract.decimals(); // 6
const balance = await usdcContract.balanceOf('0xSomeAddress');
console.log('USDC balance:', ethers.formatUnits(balance, decimals));
Setting Up a Signer to Send Transactions
A provider is read-only. To sign and send transactions, you need a Signer. There are two common approaches:
Option 1: Private Key Signer (backend / scripts)
const PRIVATE_KEY = process.env.PRIVATE_KEY; // never hardcode this
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
console.log('Wallet address:', wallet.address);
console.log('Balance:', ethers.formatEther(await wallet.provider.getBalance(wallet.address)));
Option 2: Browser Wallet (MetaMask / frontend)
// In a browser environment
const browserProvider = new ethers.BrowserProvider(window.ethereum);
// Request wallet connection
await browserProvider.send('eth_requestAccounts', []);
const signer = await browserProvider.getSigner();
console.log('Connected address:', signer.address);
Sending ETH
const tx = await wallet.sendTransaction({
to: '0xRecipientAddress',
value: ethers.parseEther('0.01'), // 0.01 ETH
});
console.log('Transaction hash:', tx.hash);
// Wait for confirmation
const receipt = await tx.wait();
console.log('Confirmed in block:', receipt.blockNumber);
console.log('Gas used:', receipt.gasUsed.toString());
console.log('Status:', receipt.status === 1 ? 'Success' : 'Failed');
Writing to a Smart Contract
const ERC20_WRITE_ABI = [
'function transfer(address to, uint256 amount) returns (bool)',
'function approve(address spender, uint256 amount) returns (bool)',
];
const tokenContract = new ethers.Contract(TOKEN_ADDRESS, ERC20_WRITE_ABI, wallet);
// Send tokens
const amount = ethers.parseUnits('100', 6); // 100 USDC (6 decimals)
const tx = await tokenContract.transfer('0xRecipientAddress', amount);
console.log('TX hash:', tx.hash);
const receipt = await tx.wait();
console.log('Transfer confirmed in block:', receipt.blockNumber);
Estimating Gas Before Sending
// Estimate gas before sending — avoids out-of-gas failures
const gasEstimate = await tokenContract.transfer.estimateGas(
'0xRecipientAddress',
amount
);
// Add 20% buffer
const gasLimit = gasEstimate * 120n / 100n;
const tx = await tokenContract.transfer('0xRecipientAddress', amount, {
gasLimit
});
Connecting to Multiple Chains
Same pattern, different endpoint URL:
const providers = {
ethereum: new ethers.JsonRpcProvider(
'https://eu.endpoints.matrixed.link/rpc/ethereum?auth=YOUR_KEY'
),
arbitrum: new ethers.JsonRpcProvider(
'https://eu.endpoints.matrixed.link/rpc/arbitrum?auth=YOUR_KEY'
),
base: new ethers.JsonRpcProvider(
'https://eu.endpoints.matrixed.link/rpc/base?auth=YOUR_KEY'
),
polygon: new ethers.JsonRpcProvider(
'https://eu.endpoints.matrixed.link/rpc/polygon?auth=YOUR_KEY'
),
};
// Read the same contract address on different chains
async function getBalanceOnAllChains(tokenAddress, abi, walletAddress) {
const results = await Promise.all(
Object.entries(providers).map(async ([chain, provider]) => {
const contract = new ethers.Contract(tokenAddress, abi, provider);
const balance = await contract.balanceOf(walletAddress);
return { chain, balance };
})
);
return results;
}
Adding a Fallback Provider
For production applications that need high availability:
const primary = new ethers.JsonRpcProvider(
'https://eu.endpoints.matrixed.link/rpc/ethereum?auth=YOUR_KEY'
);
const fallback = new ethers.JsonRpcProvider(
'https://your-fallback-rpc-url'
);
// Tries primary first, falls back automatically
const provider = new ethers.FallbackProvider([
{ provider: primary, priority: 1, weight: 2 },
{ provider: fallback, priority: 2, weight: 1 },
]);
Connecting with WebSocket
Use WebSocketProvider when you need real-time subscriptions: new block notifications, contract event streams, pending transaction monitoring.
const wsProvider = new ethers.WebSocketProvider(
'wss://eu.endpoints.matrixed.link/ws/ethereum?auth=YOUR_KEY'
);
// Listen for new blocks
wsProvider.on('block', (blockNumber) => {
console.log('New block:', blockNumber);
});
// Listen for contract events
const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, wsProvider);
contract.on('Transfer', (from, to, value, event) => {
console.log(`Transfer: ${from} → ${to}: ${ethers.formatEther(value)} ETH`);
});
For a complete WebSocket guide with reconnection handling, see: How to Listen for Ethereum Events with WebSocket
Error Handling
Always wrap RPC calls in try/catch:
async function safeGetBalance(address) {
try {
const balance = await provider.getBalance(address);
return ethers.formatEther(balance);
} catch (error) {
if (error.code === 'NETWORK_ERROR') {
console.error('RPC connection failed');
} else if (error.code === 'CALL_EXCEPTION') {
console.error('Contract call reverted');
} else {
console.error('Unexpected error:', error.message);
}
return null;
}
}
Transaction Simulation Before Sending
Use eth_call to simulate state-changing transactions before broadcasting. Catches reverts before you spend gas:
try {
// Simulate the transaction first
await tokenContract.transfer.staticCall('0xRecipient', amount);
// Simulation passed — safe to send
const tx = await tokenContract.transfer('0xRecipient', amount);
await tx.wait();
} catch (error) {
// Simulation failed — transaction would revert
console.error('Transaction would fail:', error.reason);
}
Common Patterns Quick Reference
// Check if address is a contract
const code = await provider.getCode(address);
const isContract = code !== '0x';
// Get transaction details
const tx = await provider.getTransaction(txHash);
// Get transaction receipt
const receipt = await provider.getTransactionReceipt(txHash);
const success = receipt?.status === 1;
// Wait for a transaction with timeout
const receipt = await provider.waitForTransaction(txHash, 1, 30000);
// confirmations = 1, timeout = 30 seconds
// Encode function data manually
const iface = new ethers.Interface(ABI);
const data = iface.encodeFunctionData('transfer', [recipient, amount]);
// Decode event log
const iface = new ethers.Interface(ABI);
const decoded = iface.parseLog({ topics: log.topics, data: log.data });
v5 to v6 Migration Notes
If you are upgrading from ethers.js v5:
| v5 | v6 |
|---|---|
ethers.providers.JsonRpcProvider | ethers.JsonRpcProvider |
ethers.providers.WebSocketProvider | ethers.WebSocketProvider |
ethers.providers.Web3Provider | ethers.BrowserProvider |
ethers.utils.formatEther | ethers.formatEther |
ethers.utils.parseEther | ethers.parseEther |
BigNumber | Native BigInt |
provider.getGasPrice() | provider.getFeeData() |
Start Building
BoltRPC provides HTTP and WebSocket endpoints for 20+ blockchain networks. Replace any endpoint URL in this guide with your BoltRPC URL and it works immediately across Ethereum, Arbitrum, Base, Polygon, Optimism, Avalanche, and more.
Get your free 2-week trial: trial.boltrpc.io
See all supported networks: boltrpc.io/networks
FAQ
What is the difference between a Provider and a Signer in ethers.js?
A Provider is a read-only connection to the blockchain. It can query state, get balances, read contracts, and fetch block data. A Signer has a private key and can sign transactions. To send transactions or write to contracts, you connect a Signer to a Provider.
Should I use ethers.js v5 or v6?
Use v6. It is the current major version with active maintenance. v5 still works but will receive fewer updates. The main differences are import paths (no more ethers.providers.*, ethers.utils.*) and native BigInt replacing BigNumber.
Can I use ethers.js for non-Ethereum EVM chains?
Yes. ethers.js works with any EVM-compatible chain. Just change the RPC endpoint URL. Arbitrum, Base, Polygon, Optimism, Avalanche, BNB Chain all work identically.
What is the difference between JsonRpcProvider and WebSocketProvider?
JsonRpcProvider uses HTTP: request/response, no persistent connection. Best for standard queries and transaction submission. WebSocketProvider uses WebSocket: persistent connection that supports real-time subscriptions (new blocks, contract events). Use HTTP for everything except subscriptions.
How do I handle private keys securely?
Never hardcode private keys. Load them from environment variables (process.env.PRIVATE_KEY), a secrets manager (AWS Secrets Manager, HashiCorp Vault), or a hardware wallet integration. For frontend applications, always use browser wallet providers (MetaMask) rather than handling private keys directly.