MetaMask Wallet Extension for Developers: Using MetaMask.request() and Web3.js Integration
A developer building a decentralized application faces a practical decision at the outset: how should the dApp request access to user wallets, read account balances, and sign transactions? The answer involves understanding the provider interface that MetaMask exposes through its wallet extension, the methods available through the Ethereum provider API, and the patterns that avoid common security pitfalls. MetaMask.request() is the primary mechanism for these interactions, but its correct use requires understanding not just the syntax but the sequence of operations and error handling that separates a functional integration from one that creates friction or vulnerability.
Modern Web3 applications no longer assume a single wallet or a single blockchain. A user may switch between networks, connect with multiple accounts, or temporarily disconnect. A robust dApp must detect the presence of the MetaMask wallet extension, handle account changes gracefully, respect the user’s explicit approval for sensitive operations, and signal errors clearly rather than silently failing or requesting permission repeatedly. The technical foundation is straightforward; the operational details determine whether the user experience feels native or clumsy.
Detecting and accessing the MetaMask wallet extension provider
The MetaMask wallet extension injects an Ethereum provider object into the browser’s window object at window.ethereum. This injection occurs early in the page lifecycle, but the extension itself may not be installed, may be disabled, or may be a competing provider injected by another wallet. The first step is therefore a capability check that confirms both the presence and identity of the provider. A simple existence check is insufficient; a dApp should verify that the provider claims to be MetaMask using the isMetaMask property.
The detection pattern involves checking window.ethereum and its isMetaMask flag before attempting any interaction. If the provider is not available, the dApp should offer a clear path to download MetaMask from official channels, such as the site’s main metamask wallet extension download page. A user who attempts to interact with a dApp that requires wallet connection but does not have a wallet installed should see a prompt to install it, not an error message. The distinction matters because it separates a missing prerequisite from a genuine malfunction.
Once the provider is confirmed, the dApp can request access to user accounts using the eth_requestAccounts method. This is a permission-based call that triggers a MetaMask popup asking the user to approve the connection. The user must explicitly select accounts to share with the dApp; the dApp cannot assume the user will grant access or that the first account in the wallet is the intended one. After approval, the provider returns an array of account addresses (strings in hexadecimal format). Storing this result locally allows the dApp to reference the connected account without requesting permission repeatedly.
The key distinction is between detection, permission, and state. Detection confirms the extension exists. Permission grants the dApp access to accounts. State represents which account is currently active. All three are separate concerns. A user may have multiple accounts within one MetaMask wallet, may revoke permission at any time through the extension’s settings, and may switch between accounts or networks while the dApp is running. Each of these events can trigger a listener that the dApp should handle.
Account and network change listeners in a MetaMask wallet extension integration
The Ethereum provider emits events when the user takes certain actions. Two of the most important are accountsChanged and chainChanged. These events fire when the user switches between connected accounts within MetaMask or changes the active blockchain network. A dApp that ignores these events will show stale information: it may display the balance of an account that is no longer active, or attempt to interact with a contract on the wrong network. Implementing listeners prevents that inconsistency.
The pattern is straightforward. The dApp registers a listener on the provider using window.ethereum.on(‘accountsChanged’, callback). When the user changes accounts in MetaMask, the callback executes with the new accounts array. Similarly, window.ethereum.on(‘chainChanged’, callback) fires when the network changes, passing the new chain ID as a hexadecimal string (for example, ‘0x1’ for Ethereum mainnet or ‘0xaa36a7’ for Sepolia testnet). The dApp should update its internal state, refresh any displayed balances or data, and potentially warn the user if they have switched to an unsupported network.
A common pattern is to store the current account and chain ID in component state or a global context, then update all dependent logic when either changes. If the dApp is designed to work only on a specific network, it should check the current chain immediately after account approval and guide the user to switch networks if necessary. MetaMask’s wallet_switchEthereumChain method initiates a network switch, though the user must approve the change. A dApp should not force switches without explanation; instead, it should explain why a specific network is required and offer an easy path to switch.
Cleanup is also essential. If the dApp unmounts or the user navigates away, the listeners should be removed to prevent memory leaks or stale callback references. A listener registered without cleanup can continue firing if the user interacts with the MetaMask extension, even after the dApp has been closed. In a modern framework like React, this typically means removing listeners in a useEffect cleanup function.
Requesting accounts and handling permission states
The eth_requestAccounts method is the primary way a dApp asks for permission to connect to the user’s MetaMask wallet extension. This method is asynchronous; it returns a Promise that resolves to an array of account addresses once the user approves, or rejects with an error if the user denies the request or if something goes wrong. A dApp should handle both outcomes explicitly rather than assuming success.
The typical flow begins with a “Connect Wallet” button that the user clicks. The dApp calls window.ethereum.request({ method: ‘eth_requestAccounts’ }), which triggers the MetaMask popup. The user reviews the accounts and clicks “Connect” to approve or “Cancel” to deny. If approved, the Promise resolves and the dApp stores the account addresses. If denied, the Promise rejects with an error whose code is 4001 (user rejected the request). A dApp should catch this error gracefully and simply note that the user chose not to connect, without displaying alarming error messages.
After initial connection, the dApp should check whether the user has already approved the connection by calling eth_accounts. This method returns the list of already-connected accounts without triggering a popup. If the array is empty, the user has either not connected or has revoked permission. If it contains addresses, the dApp can assume the user has previously approved and may be returning to the dApp. Reusing eth_accounts on page load avoids unnecessary permission prompts on repeat visits.
Distinguishing between “user rejected” and “provider not available” is important for user experience. If window.ethereum is undefined, the user does not have a compatible wallet installed and should be guided to download one. If eth_requestAccounts rejects with code 4001, the user deliberately chose not to connect. If it rejects with a different error, something unexpected occurred—for example, the extension encountered an internal error—and the dApp should log the error and offer a retry option.
Reading balances and making contract calls with Web3.js
Once an account is connected, the dApp often needs to read data from the blockchain: account balances, contract state, transaction history. The Web3.js library is a widely used abstraction layer for Ethereum RPC methods. It simplifies common operations by wrapping lower-level calls into higher-level functions. Web3.js can be instantiated with the MetaMask provider, allowing seamless integration between the wallet and blockchain queries.
The initialization pattern is simple: const web3 = new Web3(window.ethereum). This creates a Web3 instance that uses the MetaMask provider for all RPC calls. The dApp can now call web3.eth.getBalance(account) to fetch the balance of an account, web3.eth.getCode(contractAddress) to check if an address contains contract code, or web3.eth.call() to execute a read-only function on a smart contract. All of these operations are asynchronous; they return Promises that resolve to results or reject with errors.
Reading data does not require transaction signatures or user approval; MetaMask simply forwards the RPC request to the connected Ethereum node. However, the node connection itself can fail, be slow, or return unexpected data. A dApp should implement timeout logic and retry mechanisms for critical reads, display loading states while data is being fetched, and gracefully handle network errors. If a read operation times out or fails, the dApp should not assume the data is unavailable; it should retry or inform the user that the information could not be loaded at this moment.
For contract interactions that change state (writes), the flow is different. A dApp constructs a transaction, requests the user to sign it via eth_sendTransaction or eth_signTransaction, and waits for the signed transaction to be broadcast. Web3.js contract abstractions handle much of this construction, but the underlying mechanism remains: the user must explicitly approve each state-changing operation through a MetaMask popup.
Signing and sending transactions through the MetaMask wallet extension
When a dApp needs to write data to the blockchain—transferring tokens, calling a smart contract function, minting an NFT—it must ask the user to sign a transaction. The MetaMask wallet extension presents a confirmation popup showing the destination, amount, gas estimate, and total cost. The user can review and approve or reject. A dApp should construct these transactions carefully, provide clear descriptions of what is about to happen, and ensure the user understands the cost and consequence before requesting approval.
The two main signing methods are eth_sendTransaction and eth_sign. The eth_sendTransaction method constructs and broadcasts a transaction, returning a transaction hash. The dApp waits for this hash and can then poll the blockchain to check confirmation status. The eth_sign method signs arbitrary data and returns a signature; it is rarely used for transactions but is valuable for cryptographic operations like proving ownership of an address without spending gas.
The transaction object passed to eth_sendTransaction includes several fields: from (the user’s account), to (the recipient or contract), value (ETH to send, in wei), data (encoded contract function and parameters), gas (the gas limit), gasPrice (the price per unit of gas), and nonce (the sequence number for the account). Most dApps set reasonable defaults for gas and gasPrice, but MetaMask will estimate them if omitted. The to and data fields determine what happens; if to is a contract address and data contains an encoded function call, the transaction will execute that function.
After the user approves, the transaction is broadcast to the Ethereum network and begins mining. The dApp should not assume the transaction has succeeded immediately; it should store the transaction hash, then poll eth_getTransactionReceipt() periodically to check for a receipt. Once a receipt is available, the dApp can check the status field: 1 indicates success, 0 indicates failure (likely due to a revert condition in the contract). A successful transaction may still have been reverted if the contract logic rejected the operation.
Gas estimation is a frequent source of confusion. MetaMask can estimate gas for most transactions, but unusual contract logic or state conditions may cause the estimate to be inaccurate. A dApp should always include error handling for transactions that fail due to insufficient gas; the error will typically indicate the actual gas used and allow the user to retry with a higher limit. A dApp should not simply resubmit with a higher gas limit without understanding why the first attempt failed.
Handling network switching and EVM-compatible chains
MetaMask is no longer Ethereum-only. It supports Ethereum and EVM-compatible networks—Polygon, Arbitrum, Optimism, Avalanche, and many others—allowing users to interact with dApps on multiple chains. A sophisticated dApp may support several networks and allow users to choose which one to interact with. When a user switches networks in MetaMask, the chainChanged event fires, and the dApp should detect and respond to the change.
The wallet_switchEthereumChain method requests a network switch. If the network is already in MetaMask’s configured list, the switch happens immediately after user approval. If it is not, MetaMask will reject the request. A dApp can then use wallet_addEthereumChain to request adding a new network, providing the chain ID, name, RPC endpoint, block explorer URL, and native currency symbol. The user can approve or reject the addition. This two-step flow (try to switch, then add if necessary) allows dApps to support networks beyond Ethereum while respecting the user’s network configuration.
After a successful network switch, the chainChanged event fires and the Web3 instance continues to work with the new provider. However, contract addresses are network-specific; a token contract on Ethereum is not the same contract on Polygon. A dApp must maintain a mapping of contract addresses to network IDs and update references when the network changes. Failing to do this can cause the dApp to attempt to interact with an invalid address, resulting in silent failures or runtime errors.
For a MetaMask wallet extension user, network switching is transparent, but the dApp’s internal state must update accordingly. A common pattern is to store the currently selected network in state, update it on chainChanged, and conditionally render UI or adjust contract calls based on the active network. If the user switches to an unsupported network, the dApp should disable functionality and clearly explain which networks are supported.
Best practices for wallet extension integration and security considerations
Security is paramount when integrating a wallet extension. The MetaMask wallet extension does not control user assets directly; the user’s Secret Recovery Phrase and private keys remain under their control. However, a dApp interaction is a critical moment where malicious code or a poorly designed interface can trick the user into approving harmful transactions. A dApp should follow several defensive practices.
First, always verify user intent before requesting signatures. Display a clear preview of what the transaction will do. If the user is approving a contract allowance to spend their tokens, show the token amount and contract address. Do not hide critical details in collapsed sections or assume users will read fine print. MetaMask itself shows transaction details in its popup, but a dApp should provide context before the MetaMask approval screen appears.
Second, validate all data before use. If a user pastes a contract address or receiving address, verify it is a valid Ethereum address (40 hexadecimal characters preceded by 0x) and check it against known lists of suspicious or phishing addresses where possible. A dApp cannot prevent a user from sending funds to the wrong address, but it can warn them if the address does not match expected patterns or is flagged as a scam.
Third, use HTTPS and code signing. Serve the dApp over HTTPS to prevent man-in-the-middle attacks. If distributing the dApp as a package, sign it to ensure users can verify it has not been tampered with. The MetaMask wallet extension itself is updated by the official app store, which reduces the risk of malicious versions, but third-party integrations may be distributed through less secure channels.
Fourth, request only the permissions needed. Do not request eth_sign for operations that can use eth_signTypedData, which provides better clarity about what the user is signing. Do not request write permissions if only reads are needed. Minimize the scope of each signature request. A user who sees multiple approval popups may become fatigued and start approving without reading, which is exactly the condition a malicious dApp would exploit.
Finally, test on testnets before mainnet. Networks like Sepolia allow developers to experiment with the wallet extension integration, call contracts, and submit transactions without risking real funds. A dApp should be thoroughly tested for edge cases: network switching while a transaction is pending, rapid account changes, provider disconnection, and contract calls that revert. Mainnet should be treated as a production environment; issues discovered there are far more costly.
Troubleshooting common integration issues
A dApp that worked correctly yesterday may suddenly fail if the user updates the MetaMask wallet extension, if the page fails to refresh properly, or if an RPC endpoint becomes overloaded. Common issues and their causes help developers debug quickly. One frequent problem is that window.ethereum is undefined on page load. This typically means the MetaMask extension is not installed, the page loaded before the extension injected the provider, or the extension is disabled. A dApp should check for the provider on a slight delay or listen for a providerFound event to handle late injection.
Another issue is that eth_requestAccounts works, but subsequent calls to eth_accounts return an empty array. This suggests the user approved the connection in an older version of the dApp or a different dApp, but the current origin was not included in the approval. Clearing the browser cache or resetting MetaMask permissions in the extension settings may help, but the user should be guided to explicitly reconnect.
Transaction failures often stem from insufficient gas or a revert condition in the contract. The error message from MetaMask may be cryptic; a dApp should attempt to decode it. If the transaction reverts, inspect the contract code to understand the conditions under which the operation is rejected. A transaction that appears to hang likely indicates it is pending confirmation on the network. The dApp should monitor the transaction hash and display its status to the user rather than showing a frozen screen.
Contract calls may fail silently if the address or function signature is incorrect. Web3.js will raise an error if the method does not exist on the contract, but typos in function names or incorrect parameter encoding may bypass early validation. Testing contract calls on a testnet with known good contract addresses and functions helps isolate whether the problem is with the dApp’s construction of the call or with the contract itself.
Frequently asked questions
How do I check if a user has the MetaMask wallet extension installed?
Check for the presence and identity of the provider using if (typeof window.ethereum !== ‘undefined’ && window.ethereum.isMetaMask). The isMetaMask property distinguishes MetaMask from other wallet providers that may also inject window.ethereum. If the provider is not available, guide the user to download MetaMask from official sources.
What is the difference between eth_requestAccounts and eth_accounts?
eth_requestAccounts is a permission-based method that triggers a MetaMask popup asking the user to approve dApp connection. It returns the approved accounts if successful. eth_accounts returns the already-connected accounts without a popup. Use eth_accounts on page load to check for existing connections and eth_requestAccounts when the user clicks a “Connect Wallet” button.
Can I use MetaMask with Web3.js to read blockchain data without requiring user signatures?
Yes. Read-only operations like getting account balances, checking contract code, and calling view functions do not require signatures or user approval. Simply instantiate Web3.js with the MetaMask provider and call the desired eth_ methods. Signatures are required only for state-changing transactions like transfers and contract function calls that modify blockchain state.
How should I handle user account switching in my dApp?
Register a listener on window.ethereum.on(‘accountsChanged’, callback). When the user switches accounts in the MetaMask wallet extension, the callback receives the new accounts array. Update your dApp’s internal state, refresh balances and data, and check that the new account is valid for the current operation. Always assume the user may switch at any time.
What should I do if a transaction fails or reverts?
First, check the error message returned by eth_sendTransaction for clues about the failure. Common causes include insufficient gas, a revert condition in the contract logic, or an invalid parameter. Use eth_getTransactionReceipt with the transaction hash to check the status field (1 for success, 0 for failure). For reverts, examine the contract code and transaction parameters to understand what went wrong before retrying.

