Web3.js Contracts: Call vs. Send and the 1.x to 4.x Migration Shift
Understanding web3.eth.Contract: when to use .call() vs .send(), how to handle event logs with chunked queries, and the breaking changes from web3.js 1.x to 4.x.
23 Dec 2025, 11:02 UTC

When building Ethereum applications, the most common hurdle isn't connecting to a node—it's understanding how to interact with smart contracts correctly. Developers often struggle with why a function returns a value sometimes but fails other times, or why their code breaks when upgrading from web3.js 1.x to 4.x. This guide explores the engineering decisions behind web3.eth.Contract, focusing on the distinction between read-only calls and state-changing transactions, and the breaking changes introduced in the 4.x rewrite.
The Contract Abstraction
To interact with a contract, the library needs the contract address and the ABI (Application Binary Interface), which describes the contract's functions and parameters. The ABI acts as a map, telling JavaScript which functions exist and what arguments they expect:
// Example using Web3.js 4.x syntax
const { Web3 } = require('web3');
const web3 = new Web3('https://mainnet.infura.io/v3/YOUR_PROJECT_ID');
const abi = [/* Your Contract ABI */];
const address = '0x...';
const contract = new web3.eth.Contract(abi, address);Call vs. Send: The Golden Rule
The most important decision per method is whether to use .call() or .send(). Using the wrong one leads to wasted gas or broken logic.
- .call(): Executed locally on the node. It does not change blockchain state, is free, and returns the result immediately. Use this for view functions like checking balances.
- .send(): Broadcasts a transaction to be mined. It costs gas, must be signed, and returns a transaction hash. Use this for state-changing actions like transferring tokens.
Practical Example: ERC-20 Interaction
The following example fetches an ERC-20 balance and then transfers tokens. This code uses Web3.js 4.x, which uses native JavaScript bigint for large numbers:
async function handleToken(userAddress, toAddress, amount) {
const tokenContract = new web3.eth.Contract(erc20Abi, tokenAddress);
// 1. Read-only call (free)
const balance = await tokenContract.methods.balanceOf(userAddress).call();
console.log('Current Balance: ' + balance.toString());
// 2. Estimate gas before sending (best practice)
const gasEstimate = await tokenContract.methods.transfer(toAddress, amount)
.estimateGas({ from: userAddress });
// 3. State-changing transaction (costs gas)
const receipt = await tokenContract.methods.transfer(toAddress, amount)
.send({ from: userAddress, gas: gasEstimate });
console.log('Transaction Hash: ' + receipt.transactionHash);
}The 1.x vs. 4.x Migration Pitfalls
Web3.js 4.x is a rewrite focused on TypeScript and ESM. The primary friction points:
| Feature | Version 1.x | Version 4.x |
|---|---|---|
| Numbers | BN.js library | Native bigint |
| Imports | CommonJS | ESM-first |
| Types | Community types | TypeScript-first |
Mixing BN.js objects with native bigint causes subtle arithmetic bugs. Always check npm list web3 before porting code.
Event Logs and Provider Limits
Event retrieval via contract.getPastEvents() wraps eth_getLogs. Hosted RPC providers often cap block ranges, so chunk large queries:
// Chunked event retrieval pattern
async function getEventsChunked(contract, fromBlock, toBlock, chunkSize = 10000) {
const events = [];
for (let start = fromBlock; start <= toBlock; start += chunkSize) {
const end = Math.min(start + chunkSize - 1, toBlock);
const chunk = await contract.getPastEvents('Transfer', {
fromBlock: start,
toBlock: end
});
events.push(...chunk);
}
return events;
}Subscriptions (newHeads, logs) require a WebSocket or IPC provider; plain HTTP cannot push events—a frequent source of 'subscription not working' confusion.
Trade-offs and Verification
Never hardcode private keys; use environment variables or external signers. Test against a local node like Hardhat or Ganache, or a public testnet like Sepolia. Verify the installed version and consult the official migration guide for your major version, as API surface differs significantly between 1.x and 4.x.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.