Web3 Format Number
Format web3 values for UI and contract writes without losing precision.
Require These Tools
viemorwagmi/wagmi/actionsbignumber.js
Name Values by Representation
- primitive number value:
{variableName} - bigint from chain / before
formatUnits:{variableName}Raw - formatted string after
formatUnits:{variableName}Formatted - BigNumber instance:
{variableName}Bn
Keep the representation obvious in the variable name. This prevents mixing display values, math values, and contract-write values.
Before Formatting or Converting, Ask
- Is this value for display only, math, or a contract write?
- Do I have the correct token decimals from the token config or contract?
- Should this value stay
bigint, become a formatted string, or becomeBigNumberfor math?
Choose the Representation
| Situation | Use | Why |
|---|---|---|
| Read from contract | bigint + formatUnits |
Keep onchain precision, format only for UI/math |
| User input before write | parseUnits to bigint |
Convert display input back to raw contract value |
| Math on token values | parseToBigNumber(...) / BigNumber |
Avoid JS float precision loss and centralize fallback behavior |
| General UI display | formatted string or display helper | Keep formatting decisions out of components |
Follow These Flows
Read From Contract
Use bigint as the source of truth. Keep the raw value as *Raw, then derive a formatted value.
function useTokenBalance() {
const { data } = useReadContract(/** read user token balance */)
const userTokenBalanceRaw = data
return {
userTokenBalanceRaw,
userTokenBalanceFormatted: formatUnits(data ?? 0n, /** token decimals */ 18),
}
}
Convert Input for Contract Writes
Parse user-entered display values to raw bigint right before the write.
import type BigNumber from 'bignumber.js'
import { parseUnits } from 'viem'
function useSendToken(inputAmount: BigNumber.Value) {
const { mutateAsync } = useWriteContract()
function sendToken() {
const inputAmountRaw = parseUnits(String(inputAmount), /** token decimals */ 18)
return mutateAsync({
/** erc20 send token */
})
}
}
Do Math Safely
Align decimals first, then do math in BigNumber, then convert back to raw for contract writes.
import { parseToBigNumber } from 'xxx/bignumber'
const inputTokenRaw = 20000000000000000000n
const rewardRate = 1.5
const inputTokenFormatted = formatUnits(inputTokenRaw, 18)
const rewardTokenBn = parseToBigNumber(inputTokenFormatted).mul(rewardRate)
const shouldMintTokenRaw = parseUnits(rewardTokenBn.toString(), 18)
Create BigNumber Values
Use parseToBigNumber(value) for user input, API responses, formatUnits output, calculations, or other runtime data. It stringifies before constructing BigNumber, catches invalid input, and returns a fallback.
Only call new BigNumber(value) directly for controlled constants or values already known to be safe strings. BigNumber.js issue #148 shows the reason: a JS number with many significant digits can throw in debug or older versions, and precision can already be lost before BigNumber receives the argument. Passing strings is the reliable path for preserving decimal digits.
Display Values in UI
Use one display helper so components do not re-implement decimal or tiny-value rules.
import { formatValueToStandardDisplay } from 'xxx/formatValueToStandardDisplay'
function DisplayTokenBalance({ tokenAmount }: { tokenAmount: BigNumber.Value }) {
return <div>{formatValueToStandardDisplay(tokenAmount)}</div>
}
Read Templates Only When Needed
- For a standard token/value display helper with large-number suffixes and tiny-value formatting, read
templates/formatValueToStandardDisplay.ts. - For comma separators, shared decimal rules, USD formatting, or percentages, read
templates/formatCommonNumbers.ts. - For
BigNumberparsing, shared formatting, suffixes (K,M,B,T), or conversion helpers, readtemplates/bignumber.ts. - For very small values that should render with Unicode subscript zero positions, read
templates/unicodeSubscriptionFormat.ts.
Do NOT read templates when you only need the core viem flow (formatUnits, parseUnits, bigint) and are not implementing reusable display helpers.
Anti-Patterns
- Do not use JS
numberor float math for token or wei values. Precision loss will corrupt display values and contract-write amounts. - Do not call
new BigNumber(runtimeNumber)directly for user/API/onchain-derived values. UseparseToBigNumberso values are stringified first and invalid values fall back consistently. - Do not mix
Raw, formatted strings, andBigNumberunder the same variable meaning. Convert deliberately and rename by representation. - Do not skip or guess token decimals when calling
formatUnitsorparseUnits. Wrong decimals make both UI and writes wrong. - Do not send formatted display strings directly to contracts. Convert them back to raw
bigintwithparseUnits.
Notes
- Prefer
BigNumberfor math andbigintfor onchain raw values. - Keep
Rawasbigintonly. - Keep reusable formatting logic in helpers, not scattered across components.