Web3.js vs Ethers.js — Interact with Blockchain from JavaScript Without Losing Money
> cd .. / HUB_EDITORIALE
Trend emergenti e tecnologie

Web3.js vs Ethers.js — Interact with Blockchain from JavaScript Without Losing Money

[2026-08-07] Author: Ing. Calogero Bono
> share
Zenithby Meteora Web The operating system for your business. Social, clients, bookings and invoices in one platform. Gyms, barbers, professionals. Discover Zenith Free demo · no card

Your smart contract is deployed, the gas is paid, and now what? Now comes the real problem: getting people to use it. And this is where most projects die, because you discover that connecting a frontend to a blockchain is a minefield of broken promises, wrong gas estimates, and transactions that vanish into thin air. We, at Meteora Web, see it every day: capable developers losing themselves in the details of Web3.js or Ethers.js and burning budgets on avoidable mistakes. This guide is not an academic manual: it's the path we follow to take a dApp from theory to production, with code that works and choices that have an economic reason.

Why choose Ethers.js over Web3.js for your dApp?

The first decision that kills projects is choosing the wrong library. Web3.js is the veteran, the first to populate dApps. But it has a huge footprint, error handling that leaves much to be desired, and a learning curve that doesn't forgive. Ethers.js, on the other hand, is lighter, modular, and has a cleaner API. For us, the choice is clear: Ethers.js wins almost always, especially if you need to manage multiple networks or want a leaner frontend bundle.

But it's not just about taste. Web3.js has a history of problems with gas estimates and transaction handling on congested networks. Ethers.js, with its parseEther and formatEther, forces you to handle numbers correctly, avoiding conversion errors that cost dearly. If you're starting from scratch, don't look back: Ethers.js. If you have to maintain a legacy Web3.js project, learn to live with it, but plan the migration.

How do they compare in performance and size?

A minimal Ethers.js bundle weighs about 80KB gzipped, compared to 150KB+ for Web3.js. In a world where every kilobyte matters for SEO and load speed, this difference is felt. And we're not just talking about speed: a lighter bundle means lower hosting costs and a better user experience, which translates to more conversions. We always think in terms of ROI: a faster frontend is a frontend that sells more.

How does connecting to a blockchain network work with Ethers.js?

Before writing code, understand the concept: you don't connect to "the Internet" or "Ethereum" like a server. You connect to a node, which is a computer running the protocol and answering your questions. Your frontend talks to this node via JSON-RPC, and the library (Ethers.js) translates your calls into that language. Choosing the right node is a business decision: using a public provider like Cloudflare or a paid service like Infura or Alchemy changes reliability and costs.

Sponsored Protocol

For a serious project, never use a free public node for production. They are slow, limited, and can go down at the worst times. We always start with a managed provider, with a plan that scales with traffic. Here's how to set up a basic connection:

import { ethers } from "ethers";

// Connect to a node (e.g., Infura, Alchemy, or your own node)
const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID");

// Read the current block number
const blockNumber = await provider.getBlockNumber();
console.log("Current block:", blockNumber);

// Read the balance of an address
const address = "0x...";
const balance = await provider.getBalance(address);
console.log("Balance in ETH:", ethers.formatEther(balance));

This code works immediately. But note: getBalance returns a BigInt, not a number. Handling numbers with BigInt is the first rule to avoid calculation errors. Never do arithmetic with floats in JavaScript when dealing with wei or gwei.

Which provider should you choose for your infrastructure?

Choosing a provider is a reliability choice. Infura and Alchemy are the most well-known, but there are also options like QuickNode or Drpc. Each has its pricing and limits. For an MVP, you can start with a free plan, but plan to move to a paid plan as traffic grows. The cost of a managed provider is negligible compared to the cost of downtime that loses customers and reputation.

How to read data from a smart contract with Web3.js or Ethers.js?

Reading data from a smart contract is the most common and simplest operation. No signing required, no gas paid: you're just asking the node a question. But you need the ABI, the Application Binary Interface, which describes the contract's functions. Without ABI, the library doesn't know how to interpret the data. The ABI is a JSON generated at compile time with Hardhat or Foundry.

Here's how to read an ERC-20 token balance:

import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID");

// Minimal ABI for the balanceOf function
const abi = [
  "function balanceOf(address owner) view returns (uint256)",
  "function symbol() view returns (string)"
];

const tokenAddress = "0x..."; // Token address
const contract = new ethers.Contract(tokenAddress, abi, provider);

const holderAddress = "0x...";
const balance = await contract.balanceOf(holderAddress);
console.log("Token balance:", balance.toString());

const symbol = await contract.symbol();
console.log("Symbol:", symbol);

Note how the ABI defines the function as view: this tells the library that the call doesn't modify state and doesn't require gas. Getting the ABI definition wrong leads to decoding errors that waste hours. Always use the ABI generated by the compiler, not one written by hand.

Sponsored Protocol

How to handle decoding errors and missing data?

One of the most common errors is missing revert data or call revert exception. This happens when the contract throws an exception, perhaps because the address is invalid or the function doesn't exist. The library returns an error, but the message isn't always clear. Our advice: wrap every call in a try/catch and log the full error, not just the message. Often the answer is in the error object's details.

How to send transactions that modify blockchain state?

Here things get serious. Sending a transaction means paying gas, and gas is paid in ETH or the network's token. Before sending, you need a wallet (e.g., MetaMask) connected and the user must sign the transaction. Signing is not a detail: it's the explicit consent of the user to spend their funds. Your frontend must never handle private keys directly. Never. Always use a wallet provider like MetaMask or WalletConnect.

Here's how to send a transaction to transfer ETH:

import { ethers } from "ethers";

// Provider from the browser wallet (MetaMask)
const provider = new ethers.BrowserProvider(window.ethereum);

// Request wallet access
await provider.send("eth_requestAccounts", []);

// The signer is the user who signs
const signer = await provider.getSigner();

// Send 0.1 ETH to an address
const tx = await signer.sendTransaction({
  to: "0xrecipient",
  value: ethers.parseEther("0.1")
});

// Wait for transaction confirmation
const receipt = await tx.wait();
console.log("Transaction confirmed:", receipt.hash);

This is the basic flow. But in practice, you need to handle gas estimation, gas price, and possible network errors. If you're not careful, you can run into out of gas or transactions stuck in pending for hours. Gas is the cost of doing business on blockchain: learning to estimate it well saves you money and frustration.

Sponsored Protocol

How to estimate gas before sending a transaction?

Use the library's estimateGas function. It returns an estimate of the gas needed, but beware: it's an estimate, not a guarantee. On congested networks, gas prices can change in seconds. Here's how to do it:

const gasEstimate = await signer.estimateGas({
  to: "0xrecipient",
  value: ethers.parseEther("0.1")
});
console.log("Estimated gas:", gasEstimate.toString());

// Set a gas limit with a safety margin (e.g., 20% more)
const gasLimit = gasEstimate * 120n / 100n;

const tx = await signer.sendTransaction({
  to: "0xrecipient",
  value: ethers.parseEther("0.1"),
  gasLimit
});

This code saves you from nasty surprises. But remember: gas limit is not gas price. Gas price is maxFeePerGas and maxPriorityFeePerGas on EIP-1559 networks. Let the library calculate them, but if you want to optimize, study how the fee market works.

What security mistakes to avoid when interacting with blockchain?

Security is not an option, it's a necessity. We, at Meteora Web, have seen projects with private keys hardcoded in the frontend, exposed node endpoints, and contracts with admin functions without checks. The frontend is not the place for secrets. Anything sensitive must stay in a backend, and the frontend should only sign transactions. Always use wallets like MetaMask, which handle keys securely.

Another common mistake is not validating user input. If your form accepts an address, verify it's a valid address with ethers.isAddress(). If it accepts an amount, make sure it's a positive number. Blockchain doesn't forgive: once a transaction is sent, you can't undo it. Prevention is the only strategy.

How to protect your frontend from common attacks?

Besides input validation, watch out for:

  • Phishing: always verify your site's URL and use HTTPS. An attacker can clone your frontend and steal keys.
  • Reentrancy: if your contract has functions that call other contracts, make sure it's protected. But that's smart contract side, not frontend.
  • Interception: never send sensitive data over HTTP. Always use HTTPS and, if possible, encrypted WebSockets for real-time connections.

And don't forget backups. If your backend breaks, you need to restore everything. We know it well: security in Italian SMEs is systematically underestimated, and in the blockchain world it's even worse.

Sponsored Protocol

How to handle real-time connections with WebSocket?

dApps are not just single requests: often you need to listen to events, like a balance change or a new transaction. For this, you need a WebSocket connection, which keeps an open channel with the node. Ethers.js supports WebSocketProvider, but beware: WebSocket connections are more fragile than HTTP. If the connection drops, you need to reconnect and handle missed messages.

Here's an example of listening to contract events:

import { ethers } from "ethers";

const provider = new ethers.WebSocketProvider("wss://mainnet.infura.io/ws/v3/YOUR_PROJECT_ID");

const abi = [
  "event Transfer(address indexed from, address indexed to, uint256 value)"
];
const contractAddress = "0x...";
const contract = new ethers.Contract(contractAddress, abi, provider);

// Listen to Transfer events
contract.on("Transfer", (from, to, value, event) => {
  console.log("Transfer:", from, to, value.toString());
});

// Close the connection when no longer needed
// provider.destroy();

This code keeps you updated in real-time. But handle reconnection: if the WebSocket drops, you need to reconnect and start listening again. A common pattern is to use a reconnection interval with exponential backoff.

What are the limits of WebSockets and how to overcome them?

WebSockets have a connection limit per IP and can be blocked by some firewalls. Also, if your frontend is static, you need to ensure the WebSocket server is configured correctly. For high-frequency applications, consider services like Pusher or Socket.io, but for blockchain, Ethers.js is sufficient. Simplicity beats complexity, as long as the data volume doesn't require more.

How to test your blockchain interactions before deployment?

Testing is the only way to not burn real money. Use a test network like Sepolia or Goerli, where ETH is fake. But that's not enough: you need automated tests. We use Hardhat, which integrates a local test environment and lets you simulate transactions without costs. Here's an example of a Hardhat test:

Sponsored Protocol

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Token", function () {
  it("Should return the correct balance", async function () {
    const [owner, addr1] = await ethers.getSigners();
    const Token = await ethers.getContractFactory("MyToken");
    const token = await Token.deploy("MyToken", "MTK", 18);
    await token.waitForDeployment();

    const balance = await token.balanceOf(owner.address);
    expect(balance).to.equal(ethers.parseEther("1000000"));
  });
});

This test verifies that the contract assigns the initial balance to the deployer. It seems trivial, but it protects you from logic errors that would cost dearly. Write tests for every contract function, not just happy paths. Test error cases too, like a transfer to an invalid address.

How to use network forks to test realistic scenarios?

Hardhat allows you to fork a real network, like Ethereum mainnet, and test your code on a state identical to production. This is invaluable for verifying interaction with existing contracts, like Uniswap or Aave. With a fork, you can simulate transactions with real balances, but without spending a cent. The fork is your best friend for testing complex integrations.

What to do now to bring your dApp to production

You have the basics, now you need to act. Here are the concrete steps:

  • Choose Ethers.js for new projects, and plan migration from Web3.js if you have legacy.
  • Set up a managed provider (Infura, Alchemy) with a plan that scales. Don't use free nodes in production.
  • Write automated tests with Hardhat and test on Sepolia before touching mainnet.
  • Implement security: no keys in the frontend, input validation, mandatory HTTPS.
  • Monitor transactions and handle errors with try/catch and detailed logs.

If you want to dive deeper into the whole ecosystem, read our guide Blockchain and Web3 for Developers. And remember: a dApp that isn't tested is a dApp that loses money. We, at Meteora Web, build platforms that withstand traffic and audits. If you need us, we're here.

> share
Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere informatico, fondatore di Meteora Web e Zenith OS. System administrator e progettista di piattaforme, app e CMS proprietari, con esperienza in sviluppo full-stack, marketing digitale ed ecosistema Google.
[ Read Full Dossier ]

> METEORA_WEB // DIGITAL AGENCY

We build the digital presence your business deserves.

Websites, social media, online advertising, e-commerce and high-performance hosting, engineered with method by computer engineers in Sciacca, for all of Italy.

> MW_JOURNAL

> READ_ALL()