web3.js vs ethers.js: Which Library Should You Use?
Two libraries dominate Ethereum JavaScript development: web3.js and ethers.js. Both let you connect to an RPC endpoint, read blockchain data, interact with contracts, and send transactions. They solve the same problem in different ways.
This guide compares them directly so you can pick the right one for your project.
Quick Summary
| web3.js | ethers.js v6 | |
|---|---|---|
| Bundle size | ~590 KB (minified) | ~120 KB (minified) |
| TypeScript | Partial (v4 improved) | Full, first-class |
| BigInt | Yes (v4+) | Yes (v6) |
| Tree-shaking | Limited | Yes |
| API style | Monolithic (web3.eth.*) | Modular (Provider, Signer, Contract) |
| Maintained by | ChainSafe | Richard Moore + community |
| Best for | Legacy projects, Truffle ecosystem | New projects, TypeScript, DeFi |
Bundle Size
ethers.js is significantly smaller. For frontend applications, this directly affects page load time.
web3.js ships the full library in a single bundle. Even if you only need eth_call and eth_getBalance, you get everything.
ethers.js is tree-shakeable. A minimal setup that only reads contracts might bundle under 40 KB gzipped.
For backend scripts (Node.js), bundle size does not matter. For browser applications and DeFi frontends, ethers.js or viem wins on performance.
API Design
web3.js organizes everything under the web3 object:
import Web3 from 'web3'
const web3 = new Web3('https://eu.endpoints.matrixed.link/rpc/ethereum?auth=YOUR_KEY')
// Everything is web3.eth.*
const block = await web3.eth.getBlockNumber()
const balance = await web3.eth.getBalance('0xAddress')
const gasPrice = await web3.eth.getGasPrice()
ethers.js separates the provider (read), signer (write), and contract into distinct objects:
import { ethers } from 'ethers'
const provider = new ethers.JsonRpcProvider(
'https://eu.endpoints.matrixed.link/rpc/ethereum?auth=YOUR_KEY'
)
// Provider for reads
const block = await provider.getBlockNumber()
const balance = await provider.getBalance('0xAddress')
// Signer for writes
const wallet = new ethers.Wallet(PRIVATE_KEY, provider)
const tx = await wallet.sendTransaction({ to: '0x...', value: ethers.parseEther('0.01') })
The ethers.js separation makes the intent explicit. A function that only reads data takes a Provider. A function that signs takes a Signer. This is cleaner for large codebases and makes testing easier. Mock the provider, not the entire web3 object.
TypeScript Support
ethers.js was designed with TypeScript from early on. Contract types are inferred from the ABI:
import { ethers } from 'ethers'
// Full type inference on contract function calls
const ERC20_ABI = [...] as const
const contract = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, provider)
// TypeScript knows the return type is bigint
const balance: bigint = await contract.balanceOf('0xAddress')
web3.js v4 added improved TypeScript support, but ethers.js (and viem) still have tighter integration. If TypeScript matters to your team, ethers.js or viem is the better choice.
BigInt Handling
Both libraries now use native JavaScript BigInt for large numbers (token amounts, wei values). Older tutorials show BN.js or .toString() patterns. Those are from before BigInt support was added.
ethers.js v6:
// All numeric returns are BigInt
const balance = await provider.getBalance('0xAddress')
// balance is 1234567890000000000n (BigInt)
// Utility functions work with BigInt
const ether = ethers.parseEther('1.5') // 1500000000000000000n
const formatted = ethers.formatEther(balance) // '1.23456789'
web3.js v4:
const balance = await web3.eth.getBalance('0xAddress')
// balance is also BigInt in v4
const ether = web3.utils.toWei('1.5', 'ether')
const formatted = web3.utils.fromWei(balance, 'ether')
Contract Interaction
web3.js:
const contract = new web3.eth.Contract(ERC20_ABI, USDC_ADDRESS)
// Read
const balance = await contract.methods.balanceOf('0xAddress').call()
// Write
const tx = await contract.methods.transfer('0xTo', amount).send({
from: senderAddress,
gas: 100000,
})
ethers.js v6:
const contract = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, provider)
// Read
const balance = await contract.balanceOf('0xAddress')
// Write (needs a signer)
const contractWithSigner = contract.connect(wallet)
const tx = await contractWithSigner.transfer('0xTo', amount)
const receipt = await tx.wait()
ethers.js contract calls look like regular function calls. web3.js requires .methods.functionName().call() or .send() which is more verbose but makes the read/write distinction explicit at the syntax level.
Event Handling
web3.js:
// Poll for past events
const events = await contract.getPastEvents('Transfer', {
fromBlock: currentBlock - 1000,
toBlock: 'latest',
})
// Subscribe (requires WebSocket provider)
contract.events.Transfer({ fromBlock: 'latest' })
.on('data', event => console.log(event))
.on('error', err => console.error(err))
ethers.js v6:
// Query past events
const filter = contract.filters.Transfer()
const events = await contract.queryFilter(filter, currentBlock - 1000, 'latest')
// Subscribe (requires WebSocket provider)
contract.on('Transfer', (from, to, amount, event) => {
console.log(`Transfer: ${from} → ${to}: ${amount}`)
})
ethers.js event subscriptions pass decoded arguments directly to the callback. web3.js returns raw event objects that you decode manually.
WebSocket Support
Both support WebSocket providers for real-time subscriptions.
web3.js:
const web3 = new Web3('wss://eu.endpoints.matrixed.link/ws/ethereum?auth=YOUR_KEY')
ethers.js v6:
const provider = new ethers.WebSocketProvider(
'wss://eu.endpoints.matrixed.link/ws/ethereum?auth=YOUR_KEY'
)
Error Handling
ethers.js has a typed error system. You can check error.code against named constants:
try {
await contract.transfer('0xTo', amount)
} catch (error) {
if (error.code === 'CALL_EXCEPTION') {
console.log('Contract reverted:', error.reason)
} else if (error.code === 'NETWORK_ERROR') {
console.log('RPC connection issue')
} else if (error.code === 'INSUFFICIENT_FUNDS') {
console.log('Not enough ETH for gas')
}
}
web3.js errors are less structured. They are often plain Error objects with message strings that vary by provider.
Which Should You Use?
Choose ethers.js if:
- Starting a new project
- Using TypeScript
- Building a DeFi application or bot
- Bundle size matters (frontend)
- You want cleaner Provider/Signer separation
Choose web3.js if:
- Maintaining an existing codebase built on web3.js
- Using Truffle or web3.js-specific tooling
- Your team already knows web3.js well
Consider viem if:
- Building a React/Next.js app with wagmi
- You want maximum type safety and tree-shaking
- You are starting fresh and want the most modern API
For most new projects in 2026, ethers.js v6 or viem is the right choice. web3.js has a large installed base but is losing mindshare to both alternatives.
Connecting to BoltRPC
The same endpoint pattern works for both:
// ethers.js v6
import { ethers } from 'ethers'
const provider = new ethers.JsonRpcProvider(
'https://eu.endpoints.matrixed.link/rpc/ethereum?auth=YOUR_KEY'
)
// web3.js v4
import Web3 from 'web3'
const web3 = new Web3(
'https://eu.endpoints.matrixed.link/rpc/ethereum?auth=YOUR_KEY'
)
// WebSocket — ethers.js
const wsProvider = new ethers.WebSocketProvider(
'wss://eu.endpoints.matrixed.link/ws/ethereum?auth=YOUR_KEY'
)
// WebSocket — web3.js
const web3ws = new Web3(
'wss://eu.endpoints.matrixed.link/ws/ethereum?auth=YOUR_KEY'
)
FAQ
Can I migrate from web3.js to ethers.js gradually?
Yes. Both libraries can coexist in the same project. They each create their own provider connection. You can migrate module by module, replacing web3.js usage with ethers.js as you touch each file.
Is web3.js still actively maintained?
Yes. ChainSafe maintains web3.js and released v4 in 2023 with TypeScript improvements and BigInt support. It is not abandoned. However, ethers.js and viem have more momentum in the ecosystem for new projects.
Does ethers.js v6 break compatibility with v5?
Yes, significantly. ethers.js v6 changed the import style, replaced BigNumber with native BigInt, renamed several providers, and restructured the package. Migrating from v5 to v6 requires updating imports, removing .toBigInt() calls, and adjusting provider setup. Most tutorials written before 2023 use v5 syntax.
What about viem: is it better than ethers.js?
viem is faster and more type-safe than ethers.js in benchmarks. For React apps using wagmi, viem is the better choice since wagmi is built on it. For non-React backend code, ethers.js and viem are comparable. The choice often comes down to familiarity.
Do all Ethereum RPC providers support both libraries?
Yes. web3.js and ethers.js both use the standard Ethereum JSON-RPC spec. Any provider that speaks JSON-RPC (which all of them do) works with both libraries. The library choice is independent of which RPC provider you use.
BoltRPC works with web3.js, ethers.js, viem, and any other library that speaks Ethereum JSON-RPC. HTTP and WebSocket endpoints available for Ethereum, Arbitrum, Base, Polygon and 17 other networks.
Start your free 2-week trial: trial.boltrpc.io
Related: How to Connect Ethereum with ethers.js | How to Use viem with a Custom RPC Endpoint