Handling Asynchronous State and Events in Web3.js
Learn how to handle asynchronous smart contract interactions in Web3.js, distinguishing between .call() and .send(), and implementing event listeners to keep your UI in sync with the blockchain.
02 Nov 2025, 21:24 UTC

The Challenge of Blockchain Latency
Integrating a frontend with an Ethereum smart contract often leads to a common frustration: the UI doesn't reflect the blockchain state immediately after a transaction is sent. Because blockchain operations are asynchronous and depend on network confirmation, relying on a simple await for a transaction receipt often leaves the user staring at a frozen screen or an outdated balance.
To build a responsive dApp, you must decouple the transaction submission from the state update. The most reliable way to achieve this is by combining .send() for state changes with event listeners for UI synchronization.
Differentiating .call() and .send()
Web3.js distinguishes between reading data and changing it. Understanding this prevents unnecessary gas costs and provider prompts.
- .call(): Used for read-only functions (
vieworpurein Solidity). These execute locally on the node and return a value immediately. They do not require a transaction signature or gas. - .send(): Used for functions that modify the blockchain state. These require a wallet (like MetaMask) to sign the transaction and pay gas. They return a transaction hash immediately, but the actual state change occurs only after the block is mined.
Implementing Real-Time Event Listening
Instead of polling the contract every few seconds—which can lead to RPC rate-limiting—use the .on() method. This allows your application to react the moment a specific event is emitted by the smart contract.
For historical data, getPastEvents is used to fill the UI gap between the last time the app was open and the current block.
Worked Example: Token Transfer and Balance Update
This example assumes Web3.js v4.x and a provider like MetaMask. We will trigger a transfer and listen for a Transfer event to update the UI.
// Initialize contract with ABI and Address
const myContract = new web3.eth.Contract(ABI, contractAddress);
async function transferTokens(recipient, amount)
{
try {
// 1. Convert amount to Wei (18 decimals)
const amountInWei = web3.utils.toWei(amount, 'ether');
// 2. Send transaction (requires user signature)
// Run this in the browser context with a window.ethereum provider
const receipt = await myContract.methods.transfer(recipient, amountInWei).send({
from: currentAccount
});
console.log('Transaction mined in block:', receipt.blockNumber);
} catch (error) {
console.error('Transfer failed:', error.message);
}
}
// 3. Listen for the Transfer event to update UI globally
myContract.events.Transfer({ fromBlock: 'latest' })
.on('data', (event) => {
console.log('New Transfer detected!');
console.log('From:', event.returnValues.from);
console.log('To:', event.returnValues.to);
console.log('Amount:', web3.utils.fromWei(event.returnValues.value, 'ether'));
// Trigger UI refresh function here
updateBalanceUI();
})
.on('error', (err) => {
console.error('Event subscription error:', err);
});
Critical Engineering Trade-offs
BigInt vs. Number: Ethereum handles values in 256-bit integers. JavaScript's Number type fails at 16 digits, leading to precision loss in token balances. Always use BigInt or the web3.utils conversion methods (toWei/fromWei) when performing calculations.
Provider Reliability: Public RPC endpoints often throttle requests. If your dApp relies heavily on .on() listeners, consider using a dedicated provider (like Infura or Alchemy) or a local node (Hardhat/Ganache) for development. If a WebSocket connection drops, the listener will stop working without throwing a visible error in the UI; implementing a heartbeat check or reconnection logic is recommended.
Verification and Testing
To verify your implementation without spending real Ether:
- Deploy your contract to a local Hardhat or Ganache network.
- Connect Web3.js to
http://127.0.0.1:8545. - Trigger a
.send()operation and monitor the browser console to ensure the.on('data')callback fires only after the transaction receipt is received. - Check the Network tab in DevTools to ensure the JSON-RPC calls are returning
200 OKand not429 Too Many Requests.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.