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:
- Sending ETH via
sendTransaction. - Monitoring confirmation status with
useWaitForTransactionReceipt. - Handling UI states like loading, success, and errors.
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:
eth_requestAccounts: Requests user account access.eth_sendTransaction: Initiates the transaction.eth_getTransactionReceipt: Tracks confirmation status.
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
- ✅ Validate all inputs (addresses, amounts) before initiating transactions.
- ✅ Verify addresses using libraries like
viem’sisAddress()function. - ✅ Check wallet balance to avoid failed transactions due to insufficient funds.
Error Handling
Common errors include:
- User rejection (
error.code === 4001) - Insufficient funds (
-32603) - Gas too low (
-32000) - Pending request (
-32002)
Handle them gracefully:
- Show clear, user-friendly messages.
- Allow retry options where appropriate.
- Prevent duplicate submissions with state locking.
User Experience Optimization
- 🔄 Display loading states during signing and confirmation.
- 📊 Provide real-time transaction progress updates.
- 🧾 Show detailed transaction info (hash, network, fee) post-execution.
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.