Handle Transactions in Your dApp with MetaMask

·

Building decentralized applications (dApps) requires seamless and secure transaction handling. Whether you're using Wagmi for a React-based frontend or writing Vanilla JavaScript, integrating Ethereum Virtual Machine (EVM) transactions into your application is essential for user interaction. This guide walks you through sending, tracking, estimating gas, and managing errors in EVM transactions using MetaMask.

By mastering these core capabilities—sending transactions, tracking status, estimating gas, handling errors, and managing complex transaction patterns—you can deliver a smooth, professional user experience that stands out in the Web3 ecosystem.


Sending Transactions with Wagmi

Wagmi is a powerful React Hooks library built on top of viem, designed to simplify interactions with wallets like MetaMask. It abstracts much of the low-level complexity while giving developers full control over transaction logic.

Basic Transaction Example

A basic transaction involves sending ETH from one address to another. Here's how to implement it using useSendTransaction and useWaitForTransactionReceipt.

import { parseEther } from "viem";
import { useSendTransaction, useWaitForTransactionReceipt } from "wagmi";

function SendTransaction() {
  const { data: hash, error, isPending, sendTransaction } = useSendTransaction();
  
  const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({
    hash,
  });

  async function handleSend() {
    sendTransaction({
      to: "0x...", // Recipient address
      value: parseEther("0.1"), // 0.1 ETH
    });
  }

  return (
    <div>
      <button onClick={handleSend} disabled={isPending}>
        {isPending ? "Confirming..." : "Send 0.1 ETH"}
      </button>

      {hash && (
        <div>
          <div>Transaction Hash: {hash}</div>
          {isConfirming && <div>Waiting for confirmation...</div>}
          {isConfirmed && <div>Transaction confirmed!</div>}
        </div>
      )}

      {error && <div>Error: {error.message}</div>}
    </div>
  );
}

👉 Learn how to connect your wallet securely and start transacting today.

This example demonstrates:


Advanced Transaction with Gas Estimation

For more control, estimate gas before sending to avoid failed transactions due to insufficient gas limits.

import { parseEther } from "viem";
import {
  useSendTransaction,
  useWaitForTransactionReceipt,
  useEstimateGas,
} from "wagmi";

function AdvancedTransaction() {
  const transaction = {
    to: "0x...",
    value: parseEther("0.1"),
    data: "0x...", // Optional contract call data
  };

  const { data: gasEstimate } = useEstimateGas(transaction);

  const { sendTransaction } = useSendTransaction({
    ...transaction,
    gas: gasEstimate,
    onSuccess: (hash) => {
      console.log("Transaction sent:", hash);
    },
  });

  return (
    <button onClick={() => sendTransaction()} disabled={!gasEstimate}>
      Send with Gas Estimate
    </button>
  );
}

Using useEstimateGas helps prevent common failures by ensuring the transaction has enough gas, improving reliability and user trust.


Implementing Transactions in Vanilla JavaScript

You don’t need a framework to interact with MetaMask. Using native Ethereum JSON-RPC methods, you can build robust transaction flows directly in the browser.

Basic Transaction Using JSON-RPC

The following example uses three key RPC methods:

async function sendTransaction(recipientAddress, amount) {
  try {
    const accounts = await ethereum.request({ method: "eth_requestAccounts" });
    const from = accounts[0];
    const value = `0x${(amount * 1e18).toString(16)}`; // Convert ETH to wei (hex)

    const transaction = {
      from,
      to: recipientAddress,
      value,
      // Gas fields optional — MetaMask auto-estimates
    };

    const txHash = await ethereum.request({
      method: "eth_sendTransaction",
      params: [transaction],
    });

    return txHash;
  } catch (error) {
    if (error.code === 4001) {
      throw new Error("Transaction rejected by user");
    }
    throw error;
  }
}

function watchTransaction(txHash) {
  return new Promise((resolve, reject) => {
    const checkTransaction = async () => {
      try {
        const tx = await ethereum.request({
          method: "eth_getTransactionReceipt",
          params: [txHash],
        });

        if (tx) {
          if (tx.status === "0x1") {
            resolve(tx);
          } else {
            reject(new Error("Transaction failed"));
          }
        } else {
          setTimeout(checkTransaction, 2000); // Poll every 2 seconds
        }
      } catch (error) {
        reject(error);
      }
    };
    checkTransaction();
  });
}

Here’s how to use it in a simple HTML interface:

<input id="recipient" placeholder="Recipient Address" />
<input id="amount" placeholder="Amount (ETH)" type="number" />
<button onclick="handleSend()">Send ETH</button>
<p id="status"></p>

<script>
async function handleSend() {
  const recipient = document.getElementById("recipient").value;
  const amount = document.getElementById("amount").value;
  const status = document.getElementById("status");

  try {
    status.textContent = "Sending transaction...";
    const txHash = await sendTransaction(recipient, amount);
    status.textContent = `Transaction sent: ${txHash}`;

    status.textContent = "Waiting for confirmation...";
    await watchTransaction(txHash);
    status.textContent = "Transaction confirmed!";
  } catch (error) {
    status.textContent = `Error: ${error.message}`;
  }
}
</script>

👉 Discover tools that streamline dApp development and boost transaction efficiency.


Advanced: Adding Gas Estimation

Improve reliability by estimating gas before submission:

async function estimateGas(transaction) {
  try {
    const gasEstimate = await ethereum.request({
      method: "eth_estimateGas",
      params: [transaction],
    });

    // Add 20% buffer for safety
    return BigInt(gasEstimate) * 120n / 100n;
  } catch (error) {
    console.error("Gas estimation failed:", error);
    throw error;
  }
}

Always include a buffer to account for fluctuations in execution cost, especially when interacting with smart contracts.


Best Practices for Secure and Reliable Transactions

To ensure your dApp delivers a trustworthy experience, follow these proven guidelines.

Transaction Security

Error Handling

Common errors include:

Handle them gracefully:

User Experience Optimization


Frequently Asked Questions (FAQ)

Q: How do I detect if MetaMask is installed?
A: Check for the existence of window.ethereum:

if (typeof window.ethereum !== 'undefined') {
  console.log('MetaMask is available');
}

Q: What should I do when a user rejects a transaction?
A: Catch error code 4001 and prompt them to retry without showing technical jargon.

Q: Why does my transaction fail even after gas estimation?
A: Gas estimates are approximate. Always add a safety buffer (e.g., +20%) and validate contract logic.

Q: Can I send ERC-20 tokens using this method?
A: Yes! Use encoded contract interaction data (data field) when calling token transfer functions.

Q: How often should I poll for transaction receipts?
A: Every 2–5 seconds is ideal. Frequent polling increases load; too infrequent delays UX feedback.

👉 Explore next-gen tools to enhance your dApp’s performance and security.


Conclusion

Handling EVM transactions effectively is foundational to any dApp. Whether leveraging high-level libraries like Wagmi or working directly with JSON-RPC in Vanilla JavaScript, understanding how to send, track, estimate, and recover from errors ensures your application remains reliable and user-friendly.

By applying these patterns—alongside strong input validation, error handling, and UX design—you’ll build dApps that stand out in the competitive Web3 landscape.