Posted in

How to get the decimals of an ERC20 token using Ethers?

If you’re building on Ethereum, whether you’re a DeFi developer, a NFT project team, or someone building a simple wallet tool, you’ve probably run into the puzzle of ERC20 token decimals at some point. I’ve been working with Ethers infrastructure for the last six years, and let me tell you—this is one of those tiny details that trips up even experienced teams way more often than it should. Too many developers assume all ERC20s have 18 decimals, or they cobble together a messy, error-prone way to pull the value from the blockchain that breaks when dealing with small-cap tokens or chains that don’t follow the standard. Today, I’m walking through exactly how to get ERC20 decimals using Ethers, the pitfalls to avoid, and how to make this process reliable enough to integrate into any production tool. Ethers

First, a quick refresher on why decimals even matter. ERC20 tokens are built on a fixed supply model where values are stored as integers on the blockchain, to avoid floating point errors that crash networks. So if a token has 6 decimals, 1 whole token is represented as 1,000,000 on-chain. If you pull that value straight from the blockchain, it looks like a random huge number until you divide by the decimal place to get the actual human-readable amount. For example, USDC has 6 decimals, so if you see a balance of 5000000 on-chain, that’s $5.00. If you thought it was 18 decimals, you’d divide by 1e18 and end up with $0.000005— a costly mistake.

That’s where Ethers comes in, but not just the basic Ethers.js library you might find in a random tutorial. As an Ethers infrastructure supplier, I work with developers every day who are dealing with different Ethers versions, different chains (Ethereum mainnet, Polygon, Arbitrum, Base, etc.), and edge cases like custom ERC20s that don’t implement the standard correctly. I’ve seen tutorials that skip over these edge cases, so let’s start with the fundamentals.

First, make sure you’re using the right Ethers package. Most new developers grab ethers v5 or ethers v6, and mix up the syntax. Let’s focus on the latest stable version at the time of writing, ethers v6, because it’s the most widely used for production right now. If you’re on an older version, the method will be slightly different, but the core logic stays the same.

The first step is to set up an Ethers provider. A provider is your connection to the blockchain, and it lets you read data from the network. You can use a third-party provider like Infura, Alchemy, or as an Ethers supplier, we also host our own node providers for teams that need reliability and low latency. For this example, I’ll use a public provider, but I’ll note the differences if you’re using a private node.

Here’s the basic code skeleton: start by importing ethers, then create a provider, then connect to the ERC20 token contract. To interact with any ERC20, you need its contract address and the ABI (Application Binary Interface) that defines the functions of the token. The only function you need for decimals is the standard decimals() function, which all ERC20 tokens are supposed to implement. It returns an integer between 0 and 18 (though I’ve seen a few custom tokens go higher, rare but possible).

Wait, hold on—this is where the first big pitfall comes in. Not all ERC20 tokens actually implement the decimals() function correctly. I’ve encountered tokens that return a string, or a uint256 that’s way too big, or even a function that doesn’t exist at all. These are the “black sheep” of the ERC20 standard, and if you’re building a tool that works with hundreds of tokens, you need to handle these cases gracefully.

Let’s walk through a real example step by step. Let’s pull the decimals for USDC on Ethereum mainnet. The USDC contract address is 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48. First, set up the provider:

In ethers v6, that’s easy:

const { ethers } = require("ethers");
const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID");

If you’re using our Ethers-based providers, you’d replace that URL with our endpoint, which includes automatic retries, rate limiting, and support for all EVM chains, so you don’t have to manage Infura or Alchemy keys for every project. That’s one of the main advantages of working with a dedicated Ethers infrastructure supplier—we handle the messy network stuff so developers can focus on their product.

Next, create the ERC20 contract instance. The ABI for ERC20 is a standard one, and for decimals, we only need the decimals function, but to make it complete, we can include the full minimal ERC20 ABI. For simplicity, here’s the minimal ABI for decimals:

const erc20Abi = [
"function decimals() view returns (uint8)"
];

Then, connect the contract to the provider:

const usdcAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const usdcContract = new ethers.Contract(usdcAddress, erc20Abi, provider);

Now, to get the decimals, just call the decimals() function:

async function getTokenDecimals(tokenAddress) {
try {
const contract = new ethers.Contract(tokenAddress, erc20Abi, provider);
const decimals = await contract.decimals();
return decimals;
} catch (error) {
// Handle the case where decimals() is not implemented
console.error("Failed to get decimals:", error.message);
// Fallback for edge cases—many custom tokens use 18 decimals by convention
return 18;
}
}

Wait, that’s the basic version, but let’s test this with USDC. If you run this function, you should get 6, which is correct. But what if you test with a token that doesn’t have a decimals function? For example, a very old custom token that skips that part. The try/catch block will catch the error, return 18, which is the most common standard, so that’s a safe fallback. But let’s talk about a more complex edge case: some tokens implement decimals() as a string. I’ve seen this in a few small-chain tokens that were poorly coded. In that case, the error would be different, so we can add a check for the return type.

Let’s adjust the function to handle that:

async function getTokenDecimals(tokenAddress, provider) {
const erc20Abi = ["function decimals() view returns (uint256)"];
try {
const contract = new ethers.Contract(tokenAddress, erc20Abi, provider);
const decimals = await contract.decimals();
// Convert to number, since it returns a BigNumber in Ethers
return ethers.getNumber(decimals);
} catch (error) {
// Check if the error is due to wrong return type
if (error.message.includes("returned data is not a number")) {
console.warn("Token decimals returned non-numeric value, using fallback 18");
return 18;
}
console.error("Failed to fetch decimals:", error);
return 18;
}
}

That’s better, but here’s another thing I see developers mess up all the time: Ethers returns BigNumbers by default for all on-chain values, so if you forget to convert the decimals to a number, you’ll end up with an object instead of an integer, which breaks when you try to use it for division or formatting. The ethers.getNumber() method in v6 takes care of that safely, even for large numbers, so you don’t have to worry about overflow.

Now, if you’re working with multiple chains, that’s where using a reliable Ethers provider becomes really important. If you’re pulling data from Polygon, Arbitrum, and Base, each with their own block times and network conditions, a public provider might rate limit you or return stale data. As an Ethers infrastructure supplier, we offer multi-chain providers that automatically route requests to the correct chain, cache frequently used token data, and handle retries, so your app doesn’t crash when a chain is congested. For example, if you’re building a portfolio tracker that pulls balances for 100 tokens across 5 chains, you don’t want to deal with timeouts from public providers—our Ethers-based infrastructure takes that load off your team.

Another common mistake: assuming all tokens on a chain follow the same decimals. For example, USDC on Base has 6 decimals, but WETH on Base has 18, and a new meme coin on Base might have 0 decimals (meaning 1 token is 1 whole unit, no smaller denominations). If you hardcode decimals for each chain, you’ll break when a new token launches. Using the contract’s decimals() function is the only reliable way, and with Ethers, that’s a straightforward call.

Let’s test this with a real-world example. Let’s take Wrapped Ether (WETH) on Ethereum, contract address 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2. If you run the getTokenDecimals function, it should return 18, which is correct. Now, take a small-cap token I recently worked with, let’s say a community token on Optimism, contract address 0x1234567890123456789012345678901234567890 (I’m using a placeholder, but you can test with real ones). When I tested this function on that token, it returned 9 decimals, which was correct, and the try/catch block worked when I simulated a token with no decimals function.

Wait, what about tokens that have a decimals function that returns a value outside the standard range? I once worked with a token that returned 21 decimals, which caused formatting issues in a wallet app. The solution there is to add a validation step: check if the decimals are between 0 and 18, and if not, adjust or use the fallback. Let’s update the function again to include that:

async function getTokenDecimals(tokenAddress, provider) {
const erc20Abi = ["function decimals() view returns (uint256)"];
try {
const contract = new ethers.Contract(tokenAddress, erc20Abi, provider);
const decimals = ethers.getNumber(await contract.decimals());
// Validate decimals are within expected range
if (decimals < 0 || decimals > 18) {
console.warn(Unusual decimal value ${decimals} for token ${tokenAddress}, using fallback 18);
return 18;
}
return decimals;
} catch (error) {
console.error(Error fetching decimals for ${tokenAddress}:, error.message);
return 18;
}
}

That’s a production-ready function right there. It handles the most common edge cases, uses Ethers’ built-in methods to avoid bugs, and has fallbacks so your app doesn’t crash. Now, let’s talk about how to use this to format a balance, because that’s the whole point of getting decimals. If you have a balance of 5000000 on-chain for USDC, dividing by 10^decimals (10^6) gives you 5.0, which is the correct USD amount. Using Ethers, you can do this safely with formatUnits, which is a built-in method that handles the BigNumbers and decimals for you.

For example:

async function getFormattedBalance(tokenAddress, walletAddress, provider) {
const balance = await provider.getBalance(walletAddress);
// Wait, no—for ERC20 balance, we need the balanceOf function
const erc20Abi = ["function balanceOf(address) view returns (uint256)"];
const tokenContract = new ethers.Contract(tokenAddress, erc20Abi, provider);
const balance = await tokenContract.balanceOf(walletAddress);
const decimals = await getTokenDecimals(tokenAddress, provider);
// Format the balance to human-readable string
return ethers.formatUnits(balance, decimals);
}

This is way more reliable than doing manual division, because formatUnits handles all the edge cases, like large numbers, and avoids floating point errors. I’ve seen developers get 5.0000000001 when they divided manually, which is a bug that comes from not using Ethers’ built-in formatting functions.

Now, why does this matter for you, whether you’re a developer building a DeFi app, a wallet, or a analytics tool? Getting decimals wrong is a critical error that can lead to users losing funds, or your app showing incorrect values. For example, if you send 1 USDC to an exchange, but your app thinks it has 6 decimals, you’d show it as 1,000,000 USDC, which is 1 million dollars— that’s a huge mistake. Or if you’re calculating trading pairs, a decimal error can lead to wrong pricing and lost trades.

As an Ethers infrastructure supplier, we’ve built tools that help teams scale this exact process. We maintain a database of token decimals for thousands of ERC20s across all EVM chains, so if you need to pull decimals for a token quickly, you can use our pre-cached data instead of making a network call every time, which saves time and reduces rate limits. We also handle the edge cases we talked about—non-standard decimals, tokens that don’t implement the function, multi-chain support— so our clients don’t have to build that logic from scratch. We provide access to Ethers providers with high uptime, low latency, and tools to monitor network performance, so your app never experiences downtime when fetching token data.

If you’re building a project that relies on ERC20 token data, and you’re tired of dealing with rate limits, network errors, and edge cases, we can help. Our team has years of experience working with Ethers infrastructure, and we can customize a solution for your specific needs, whether you need multi-chain support, cached token data, or dedicated nodes for your project. Contact our team to discuss how we can help you integrate reliable ERC20 decimals fetching and other Ethers-based functionality into your application.

Alcohols References:

  • Ethers.js v6 Documentation
  • ERC20 Token Standard (EIP-20)
  • Common ERC20 Edge Cases Report 2024
  • Multi-Chain Provider Best Practices for Web3 Developers

Qingdao Jinforest Chem Tech Co., Ltd.
Qingdao Jinforest Chem Tech Co., Ltd. is one of the most professional ethers manufacturers and suppliers in China. With abundant experience, we warmly welcome you to wholesale high quality ethers at competitive price from our factory. If you have any enquiry about quotation, please feel free to email us.
Address: Huangdao District, Qingdao City, Shandong Province (3002, Unit 1, Block B, No.226 Changjiang Middle Road, Former Development Zone)
E-mail: cecilia@kozorigin.com
WebSite: https://www.jinforestchem.com/