Skip to content

Web3.js: Interacting with Ethereum from JavaScript

DodaTech Updated 2026-06-22 7 min read

In this tutorial, you'll learn Web3.js for connecting to Ethereum nodes, reading contract states, sending transactions, and listening to events from JavaScript. Why it matters: Web3.js is the original JavaScript library for Ethereum interaction, used by thousands of dApps to bridge frontend interfaces with smart contracts on the blockchain. By the end, you'll build a complete JavaScript application that interacts with Ethereum.

Web3.js is a collection of libraries that allow you to interact with a local or remote Ethereum node using HTTP, IPC, or WebSocket connections from JavaScript applications.

Setting Up Web3.js

Install Web3.js via npm and connect to an Ethereum node.

npm init -y
npm install web3
const Web3 = require("web3");

// Connect to a public Ethereum node
const web3 = new Web3("https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY");

// Verify connection
web3.eth.net.isListening().then(console.log).catch(console.error);

// Expected output: true

For local development with Ganache:

const web3 = new Web3("http://127.0.0.1:7545");

async function checkConnection() {
  const isListening = await web3.eth.net.isListening();
  console.log("Connected:", isListening);
  
  const blockNumber = await web3.eth.getBlockNumber();
  console.log("Latest block:", blockNumber);
  
  const accounts = await web3.eth.getAccounts();
  console.log("Available accounts:", accounts);
}

checkConnection();
// Expected output:
// Connected: true
// Latest block: 0
// Available accounts: ['0x90F8...', '0xFFcf...', ...]
flowchart LR
  A[JavaScript dApp] --> B[Web3.js Library]
  B --> C[Provider]
  C --> D[Ethereum Node
Infura/Alchemy/Local] D --> E[Blockchain] B --> F[Contract ABI + Address] F --> G[Contract Methods
call() - read] G --> H[send() - write] F --> I[Events
Subscribe / Past]

Reading Contract State

Use the call() method to read data from a contract without paying gas. This simulates the transaction locally.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract Token {
    string public name = "MyToken";
    string public symbol = "MTK";
    uint8 public decimals = 18;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    
    constructor() {
        totalSupply = 1000000 * 10**18;
        balanceOf[msg.sender] = totalSupply;
    }
}
const fs = require("fs");

// Load ABI and address
const contractABI = JSON.parse(
  fs.readFileSync("./build/contracts/Token.json", "utf8")
).abi;

const contractAddress = "0x8CdaF0CD259887258Bc13a92C0a6dA92698644C0";
const tokenContract = new web3.eth.Contract(contractABI, contractAddress);

async function readContractState() {
  // Read token details (call - no gas)
  const name = await tokenContract.methods.name().call();
  const symbol = await tokenContract.methods.symbol().call();
  const totalSupply = await tokenContract.methods.totalSupply().call();
  const balance = await tokenContract.methods.balanceOf("0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1").call();
  
  console.log(`Token: ${name} (${symbol})`);
  console.log(`Total supply: ${totalSupply}`);
  console.log(`Balance: ${web3.utils.fromWei(balance, "ether")} ${symbol}`);
}

readContractState();
// Expected output:
// Token: MyToken (MTK)
// Total supply: 1000000000000000000000000
// Balance: 1000000 MTK

Sending Transactions

Use the send() method to modify contract state. This requires gas and must be signed by a private key.

const senderAddress = "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1";
const privateKey = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d";

// Transfer tokens
const recipient = "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0";
const amount = web3.utils.toWei("100", "ether");

async function sendTransaction() {
  // Create transaction data
  const txData = tokenContract.methods.transfer(recipient, amount).encodeABI();
  
  // Get nonce and gas price
  const nonce = await web3.eth.getTransactionCount(senderAddress);
  const gasPrice = await web3.eth.getGasPrice();
  
  // Build transaction
  const tx = {
    from: senderAddress,
    to: contractAddress,
    data: txData,
    nonce: nonce,
    gas: 100000,
    gasPrice: gasPrice,
    chainId: 1337,  // Ganache chain ID
  };
  
  // Sign and send
  const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
  const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  
  console.log("Transaction hash:", receipt.transactionHash);
  console.log("Block number:", receipt.blockNumber);
  console.log("Gas used:", receipt.gasUsed);
  
  // Verify new balances
  const senderBalance = await tokenContract.methods.balanceOf(senderAddress).call();
  const recipientBalance = await tokenContract.methods.balanceOf(recipient).call();
  console.log(`Sender balance: ${web3.utils.fromWei(senderBalance, "ether")}`);
  console.log(`Recipient balance: ${web3.utils.fromWei(recipientBalance, "ether")}`);
}

sendTransaction();
// Expected output:
// Transaction hash: 0x7a3f...c9e2
// Block number: 3
// Gas used: 51728
// Sender balance: 999900
// Recipient balance: 100

Listening to Events

Subscribe to contract events for real-time updates or query past events.

// Subscribe to Transfer events
const eventSubscription = tokenContract.events.Transfer({
  filter: { from: senderAddress },  // Optional filter
  fromBlock: 0,  // Start from genesis
});

// Listen for new events
eventSubscription.on("data", (event) => {
  console.log("New Transfer event:");
  console.log(`  From: ${event.returnValues.from}`);
  console.log(`  To: ${event.returnValues.to}`);
  console.log(`  Value: ${web3.utils.fromWei(event.returnValues.value, "ether")}`);
  console.log(`  Block: ${event.blockNumber}`);
  console.log(`  Tx: ${event.transactionHash}`);
});

// Handle errors
eventSubscription.on("error", console.error);

// Get past events (already happened)
async function getPastEvents() {
  const pastEvents = await tokenContract.getPastEvents("Transfer", {
    filter: {},
    fromBlock: 0,
    toBlock: "latest",
  });
  
  console.log(`Found ${pastEvents.length} past Transfer events`);
  pastEvents.forEach((event, i) => {
    console.log(`${i + 1}. ${event.returnValues.from} -> ${event.returnValues.to}: ${web3.utils.fromWei(event.returnValues.value, "ether")}`);
  });
}

// Expected output:
// New Transfer event:
//   From: 0x90F8...
//   To: 0xFFcf...
//   Value: 100
//   Block: 3
//   Tx: 0x7a3f...

Error Handling and Utilities

Web3.js provides utilities for address validation, unit conversion, and hashing.

const Web3 = require("web3");
const web3 = new Web3();

// Address validation
function validateAddress(address) {
  const isValid = web3.utils.isAddress(address);
  console.log(`${address} is ${isValid ? "valid" : "invalid"}`);
  return isValid;
}

// Expected: true
validateAddress("0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1");
// Expected: false
validateAddress("0x1234");

// Unit conversion
console.log(web3.utils.fromWei("1000000000000000000", "ether"));
// Expected: 1

console.log(web3.utils.toWei("2.5", "ether"));
// Expected: 2500000000000000000

console.log(web3.utils.fromWei("50000000000", "gwei"));
// Expected: 50

// Hashing
console.log(web3.utils.sha3("hello"));
// Expected: 0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8

console.log(web3.utils.keccak256("Transfer(address,address,uint256)"));
// Expected: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef

// Transaction error handling
async function safeTransaction() {
  try {
    const result = await tokenContract.methods.transfer("0x0000", 100).call();
  } catch (error) {
    if (error.message.includes("revert")) {
      console.error("Transaction reverted:", error.message);
    } else if (error.message.includes("insufficient funds")) {
      console.error("Not enough ETH for gas");
    } else if (error.message.includes("nonce too low")) {
      console.error("Nonce mismatch - transaction already submitted");
    } else {
      console.error("Unknown error:", error.message);
    }
  }
}

// Expected output: Transaction reverted: Returned error: VM Exception while processing transaction: revert

Common Errors and Misunderstandings

1. Missing ABI or Wrong Address

Using the wrong contract address or mismatched ABI causes call or send to fail. Always verify the deployed contract address and regenerate the ABI after compilation.

2. Private Key Exposure

Never hardcode private keys in frontend code. Use MetaMask or wallet providers for user signing. Only use private keys in backend scripts.

3. Gas Estimation Failures

Underestimating gas causes out-of-gas errors. Use estimateGas() before sending or set a higher gas limit.

4. Nonce Management

Manual nonce management is error-prone. Use wallet providers (MetaMask) that handle nonces automatically.

5. Event Subscription Memory Leaks

Unsubscribe from events when components unmount. Failing to do so causes memory leaks and duplicate event handlers.

Practice Questions

  1. What is the difference between call() and send() in Web3.js? call() simulates a transaction locally without gas cost and cannot modify state. send() broadcasts a signed transaction that modifies state and costs gas.

  2. How do you convert between ETH and Wei in Web3.js? Use web3.utils.toWei(amount, "ether") to convert ETH to Wei and web3.utils.fromWei(amount, "ether") to convert Wei to ETH.

  3. What information does a transaction receipt contain? It contains the transaction hash, block number, gas used, status (success/failure), logs (events), and contract address (for deployments).

  4. How do you subscribe to contract events? Use contract.events.EventName({ filter, fromBlock }).on("data", handler). Filter uses indexed event parameters.

  5. What is the purpose of the contract ABI? The ABI (Application Binary Interface) describes the contract's functions, events, and their parameters. Web3.js uses it to encode/decode contract interactions.

Challenge

Build a Node.js CLI tool using Web3.js that monitors a wallet address for incoming ERC-20 token transfers, logs them to a CSV file with timestamps, and sends an email alert when a transfer exceeds a configurable threshold amount.

Real-World Task

Create a Web3.js-powered dashboard with React that connects to an Ethereum node, displays the latest block, tracks gas prices, shows a user's ETH and token balances, and allows sending ETH to any address with transaction status display. Use React hooks for real-time updates.

Frequently Asked Questions

Is Web3.js still maintained?

Yes, Web3.js v4 is actively maintained by the ChainSafe team. It includes TypeScript support, improved performance, and modern JavaScript features. However, ethers.js has gained popularity for its cleaner API and better documentation.

Can Web3.js work with Layer 2 networks?

Yes. Web3.js connects to any EVM-compatible JSON-RPC endpoint. Configure the provider URL for Arbitrum, Optimism, Polygon, or any other EVM L2. The API is identical; only the network configuration changes.

How do I handle MetaMask in Web3.js?

Use window.<a href="/cryptocurrency/ethereum/">ethereum</a> (MetaMask's injected provider) with Web3.js: const web3 = new Web3(window.<a href="/cryptocurrency/ethereum/">ethereum</a>). Request account access with <a href="/cryptocurrency/ethereum/">ethereum</a>.request({ method: "eth_requestAccounts" }).

Next Steps

After mastering Web3.js, explore ethers.js for a modern alternative, then build full-stack dApps with React integration and MetaMask wallet connection.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro