How MetaMask exposes a provider (EIP-1193)
MetaMask injects a provider on window.ethereum in modern browsers. Older sites sometimes look for window.web3; prefer the new API.
Key methods you'll use:
- provider.request({ method: 'eth_requestAccounts' }) — ask the user to connect accounts.
- provider.request({ method: 'eth_chainId' }) — returns chain ID as a hex string.
- provider.on('accountsChanged', handler) — event when user switches accounts.
- provider.on('chainChanged', handler) — event when network changes (reload often recommended).
(Yes, chain IDs are hex — remember to convert when matching.)
web3.js: connect MetaMask (step by step)
Below is a minimal example showing javascript connect MetaMask and set up web3.js.
import Web3 from 'web3';
async function connectMetaMask() {
if (typeof window.ethereum === 'undefined') {
throw new Error('MetaMask not installed');
}
const provider = window.ethereum;
try {
// Request account access
const accounts = await provider.request({ method: 'eth_requestAccounts' });
const web3 = new Web3(provider);
// Use web3 as usual
const balance = await web3.eth.getBalance(accounts[0]);
console.log('Connected account:', accounts[0]);
console.log('Balance (wei):', balance);
return { web3, account: accounts[0] };
} catch (err) {
// User rejected or other error
console.error('Connection error', err);
throw err;
}
}
Error code 4001 means the user rejected the request. Handle it politely in the UI.
React: connect MetaMask (hook pattern)
In React, a simple hook keeps state tidy and subscribes to provider events. What I've found: handling account and chain changes early prevents weird UX where the dApp is still tied to an old account.
import { useState, useEffect, useCallback } from 'react';
import Web3 from 'web3';
export function useMetaMask() {
const [web3, setWeb3] = useState(null);
const [account, setAccount] = useState(null);
const [chainId, setChainId] = useState(null);
useEffect(() => {
const provider = window.ethereum;
if (!provider) return;
const web3Instance = new Web3(provider);
setWeb3(web3Instance);
provider.request({ method: 'eth_accounts' }).then(accounts => {
if (accounts.length) setAccount(accounts[0]);
});
provider.request({ method: 'eth_chainId' }).then(id => setChainId(id));
const handleAccounts = (accounts) => setAccount(accounts[0] || null);
const handleChain = (id) => {
setChainId(id);
// Some apps reload on chain change to reset state
// window.location.reload();
};
provider.on('accountsChanged', handleAccounts);
provider.on('chainChanged', handleChain);
return () => {
provider.removeListener('accountsChanged', handleAccounts);
provider.removeListener('chainChanged', handleChain);
};
}, []);
const connect = useCallback(async () => {
if (!window.ethereum) throw new Error('No provider');
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
setAccount(accounts[0]);
}, []);
return { web3, account, chainId, connect };
}
Use this hook in a component to show a connect button and the current account.
Quick API reference (cheat sheet)
| Method / Event |
Purpose |
Example / Note |
| eth_requestAccounts |
Ask user to connect accounts |
provider.request({ method: 'eth_requestAccounts' }) |
| eth_accounts |
Get already-connected accounts |
provider.request({ method: 'eth_accounts' }) |
| eth_chainId |
Read chain ID (hex) |
provider.request({ method: 'eth_chainId' }) |
| wallet_switchEthereumChain |
Ask wallet to switch network |
params: [{ chainId: '0x1' }] |
| wallet_addEthereumChain |
Add custom network to wallet |
full chain params required |
| accountsChanged (event) |
Fired when user switches accounts |
provider.on('accountsChanged', fn) |
| chainChanged (event) |
Fired when network changes |
provider.on('chainChanged', fn) |

Common pitfalls & troubleshooting
- provider is undefined: MetaMask not installed or blocked by an extension. Link users to install-metamask-extension.
- App still shows old account after switch: subscribe to accountsChanged. Short sessions solve state drift.
- Wrong network: the provider returns a chainId you didn't expect. Offer a UI to request a chain switch (see below) and link to unsupported-networks.
- Local test nodes: if you use Ganache, set RPC and chainId correctly and see connect-ganache-local.
For common connect button issues see connect-button-troubleshoot and troubleshooting-dapp-connections.
Security notes for developers and users
MetaMask is a software wallet (hot wallet). That means convenience and risk coexist. I once approved an unlimited token allowance by accident; I had to revoke it through an external tool. Learn from my mistake: always let users inspect approvals before signing.
Developer checklist:
- Never request signatures for actions that users can't verify. Ask for minimal scopes.
- Display readable summaries of transactions (amounts, recipient, token contract address).
- Use third-party transaction simulation services if you want to show estimated outcomes (for example, before sending a complex DeFi call).
User checklist:
Advanced: switch/add networks, EIP-1559, L2s
Programmatic chain switching is supported with wallet_switchEthereumChain. Example:
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: '0x89' }], // Polygon
});
If the chain is unknown to the wallet you may receive error 4902. Then call wallet_addEthereumChain with the chain parameters.
EIP-1559 is the gas model you’ll encounter on many EVM-compatible networks. When you build transactions you can set maxFeePerGas and maxPriorityFeePerGas instead of legacy gasPrice. For Layer 2s (L2) gas behavior and savings, see layer2-networks and gas-fees-eip1559.
Account abstraction and smart contract wallets can interact with MetaMask, but some flows require custom signing and relayers. Explore smart-contract-wallets-aa if you plan to support gasless experiences.
FAQs
Q: Is it safe to keep crypto in a hot wallet?
A: Hot wallets trade some security for convenience. They’re fine for daily use and DeFi interactions. For large holdings, consider cold storage or a hardware wallet (see connect-ledger). What I've found: split funds — daily spending in a hot wallet, longer-term holdings in cold storage.
Q: How do I revoke token approvals?
A: MetaMask doesn't always expose a one-click revocation UI. Use a trusted approval manager or review contract allowances on-chain. See our guide token-approvals-revoke for safe steps.
Q: What happens if I lose my phone?
A: If you used a seed phrase to create the wallet, restore on another device with the seed phrase. If you relied on cloud or social recovery, follow those recovery guides. Read backup-recovery-seed and backup-cloud-vs-paper for options.
Conclusion and next steps
Connecting MetaMask with web3.js and React requires a few explicit steps: detect provider, request accounts, instantiate web3, and subscribe to provider events. In my experience, the small upfront work saves bugs later. Want to keep building? Start a local node (connect-ganache-local), or explore programmatic network switching (add-custom-network) and developer APIs (metamask-api-connect, developers-connect).
But don't rush key management. And test all flows on testnets first.
Happy building — and if you hit a specific error, check troubleshooting-dapp-connections or ask here and I’ll try to help.