Math in JavaScript and DeFi

Math in JavaScript becomes a real constraint in DeFi, where floating-point quirks, precision errors, and integers that don't fit in Number all show up in token balances. I handle it with bignumber.js, native BigInt, and viem utilities, depending on the kind of number.

Math in JavaScript and DeFi

Floating-point math in JavaScript

JavaScript uses IEEE 754 floating-point arithmetic, which produces surprising results for ordinary decimal operations:

console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.3 - 0.1); // 0.19999999999999998

Why rounding errors matter in DeFi

DeFi applications need precise calculations for token balances, staking rewards, and interest rates, and a small rounding error repeated across many transactions becomes a discrepancy someone can see in their balance or exploit.

Fixed decimals in token amounts

Most ERC-20 tokens and DeFi protocols use fixed decimal places instead of floating-point numbers. Balances are stored in their smallest unit, such as wei for ETH or satoshis for BTC, and formatted for people only at display time.

Converting token amounts

const tokenDecimals = 18;
const rawBalance = BigInt("1000000000000000000"); // 1 ETH in wei
const formattedBalance = Number(rawBalance) / 10 ** tokenDecimals;
console.log(formattedBalance); // 1.0

Converting through Number works for this value, but it reintroduces floating-point error as soon as the amounts get large or fractional. A big number library like bignumber.js keeps decimal math consistent and readable when the amounts are money.

Why I use bignumber.js

It covers what decimal money math needs:

  • Arbitrary precision avoids floating-point errors.
  • Performance is better than alternatives like decimal.js.
  • Size is smaller than the other full-featured libraries.
  • Maintenance is active and the library is widely used.

When an alternative fits better

Other libraries win in narrower cases:

  • big.js is smaller and faster when you only need basic decimal math.
  • decimal.js has more capabilities when you need advanced math functions.
  • Native BigInt is enough when you only work with large integers.

Safe arithmetic with bignumber.js

import BigNumber from "bignumber.js";
 
const a = new BigNumber("0.1");
const b = new BigNumber("0.2");
const sum = a.plus(b);
 
console.log(sum.toString()); // "0.3"

The result is exact because bignumber.js works in decimal strings instead of binary floating point.

EVM amounts with viem utilities

For Ethereum transactions I keep amounts as bigint end to end and use viem's parseUnits and formatUnits at the edges, so conversion between human input and the token's smallest unit happens once, without passing through Number:

import { formatUnits, parseUnits } from "viem";
 
const amount = parseUnits("1.5", 18); // 1500000000000000000n
const display = formatUnits(amount, 18); // "1.5"

Related writing