One of the most curious things about modern programming languages is that when someone decides to create a new one, lots of thinking about the accepted data types and internal auxiliary libraries takes place.

Think about all the programming languages you've worked with before. How many ways to deal with dates and times do they have? Most of them will probably release at least one way to deal with such data types because it is a very present type in a developer's programming life.

What happened to the money, then? Banks, brokers, online shopping, etc. need to handle money programmatically. And it's been like that for a long time.

Because of the lack of representativity, money types are handled in many different ways, depending on the language you're using. Consequently, some pitfalls show up.

In this article, we will explore these common pitfalls in greater detail and the best options to deal with money in JavaScript.

Commons Pitfalls

Before we dive into the pitfalls, let's first understand what is required to perform monetary calculations.

Since 2002, when Martin Fowler released his acclaimed book titled Patterns of Enterprise Application Architecture, we have a great model to deal with monetary values. It all comes down to two properties, amount and currency, and several expected operations, including _+, -, *, /, >, >=, <, <=, and =.

Think about them for a bit. If we stop seeing money as a simple number and start viewing it as a data structure composed of two essential properties and some methods to deal with comparison, conversion, and calculations, then we're tackling most of the problems involving this data type.

In other words, to do monetary calculations, you will always need an amount and a currency, as well as a way to perform operations on them (i.e., via methods/functions).

From a JavaScript perspective, a Money object that can, for example, hold the two props and expose some functions for calculations would do the job.

Don't Use a Floating Point

When dealing with money you'll need to store cents as well. For many devs, storing such values into decimal numbers is the right decision because there are decimal places.

Usually, they're represented as a unit of a power of 10:

10² = 100 cents in a dollar
10³ = 1000 cents in 10 dollars
...

However, representing money as floating-point numbers in a computer presents some problems, as we've seen here.

Floating-point numbers exist through different arithmetic on your computer. Since your computer makes use of the binary system to store decimal numbers, you'll eventually produce inconsistent results with your calculations:

0.2233 + 0.1 // results in 0.32330000000000003

This happens because the computer tries to round off as much as it can to obtain the best result. It also cuts off numbers that are too large, such as periodic tithes, for example.

You could decide to round the result of the previous operation by yourself via, for example, Math.ceil:

Math.ceil(0.2233 + 0.1) // results in 1

However, this approach would still be problematic because you'd lose a couple of pennies during the process. Depending on the type of application you're developing, a loss like that could represent a lot of lost money for customers or your business.

Because of these problems, representing money as a float object is not a recommended approach. If you're still interested in knowing more about the specifics of this issue, I strongly recommend reading Oracle's article: What Every Computer Scientist Should Know About Floating-Point Arithmetic.

Don't Use Number Either

Like in many other languages, a Number is a primitive wrapper object that is used when developers need to represent or manipulate numbers, from integers to decimals.

Additionally, because it is adouble-precision 64-bit binary format IEEE 754 value, it also presents the same threat we just talked about in the previous section.

Furthermore, Number also lacks one of Fowler's conditions to create a perfect monetary structure: currency. It would be perfectly fine if your application currently only deals with one currency. However, it could be dangerous if things change in the future.

The Intl API

The ECMAScript Internationalization API is a collective effort to provide standardized formatting for international purposes. It allows applications to decide which functionalities they need and how they will be approached.

Among the many features provided, we have number formatting, which also embraces monetary value formatting based on the specified locale.

Take a look at the following example:

var formatterUSD = new Intl.NumberFormat('en-US');
var formatterBRL = new Intl.NumberFormat('pt-BR');
var formatterJPY = new Intl.NumberFormat('ja-JP');

console.log(formatterUSD.format(0.2233 + 0.1)); // logs "0.323"
console.log(formatterBRL.format(0.2233 + 0.1)); // logs "0,323"
console.log(formatterJPY.format(0.2233 + 0.1)); // logs "0.323"

We're creating three different formatters passing different locales for US, Brazilian, and Japanese currencies, respectively. This is great to see how powerful this API is in terms of embracing both the amount and the currency at the same time and performing flexible calculations on them.

Note how the decimal system changes from one country to another and how the Intl API properly calculated the result of our monetary sum for all the different currencies.

If you'd like to set the maximum number of significant digits, simply change the code to:

var formatterUSD = new Intl.NumberFormat('en-US', {
  maximumSignificantDigits: 2
});

console.log(formatterUSD.format(0.2233 + 0.1)); // logs "0.32"

This is what usually happens when you pay for gas at a gas station.

The API can even allow you to format a monetary value, including the currency sign of the specific country:

var formatterJPY = new Intl.NumberFormat('ja-JP', {
  maximumSignificantDigits: 2,
  style: 'currency',
  currency: 'JPY'
});

console.log(formatterJPY.format(0.2233 + 0.1)); // logs "¥0.32"

Furthermore, it allows the conversion of various formats, such as speed (e.g., kilometer-per-hour) and volume (e.g., _liters). At this link, you may find all the available options for the Intl NumberFormat.

However, it's important to pay attention to the browser compatibility limitations of this feature. Since it is a standard, some browser versions won’t support part of its options, such as Internet Explorer and Safari.

For these cases, a fallback approach would be welcome if you're willing to support your app on these web browsers.

Dinero.js, Currency.js, and Numeral.js

However, there are always great libraries that the community develops to support missing features, such as dinero.js, currency.js, and numeral.js.

There are more available on the market, but we will focus on these three because they represent a significant percentage of developers using currency formatting features.

Dinero.js

Dinero.js is a lightweight, immutable, and chainable JavaScript library developed to work with monetary values and enables global settings, extended formatting/rounding options, easy currency conversions, and native support to Intl.

Installing it is as easy as running a single command:

npm install dinero.js

One of the greatest benefits of using this library is that it completely embraces Fowler's definition of money, which means it supports both amount and currency values:

const money = Dinero({ amount: 100, currency: 'USD' })

Furthermore, it also provides default methods to deal with monetary calculations:

const tax = Dinero({ amount: 10, currency: 'USD' })
const result = money.subtract(tax) // returns new Dinero object

console.log(result.getAmount()) // logs 90

It's important to state that Dinero.js doesn't deal with cents separately. The amounts are specified in minor currency units, depending on the currency you're using. If you're using USD, then money is represented in cents.

To help with the formatting part, it provides us with the toFormat() method, which receives a string with the currency pattern with which you'd like to format:

Dinero({ amount: 100 }).toFormat('$0,0') // logs "$1"
Dinero({ amount: 100000 }).toFormat('$0,0.00') // logs "$1,000.00"

You're in control over how the library handles the formats. For example, if you're dealing with currencies that have a different exponent (i.e., more than two decimal places), you can explicitly define the precision, as shown below:

Dinero({ amount: 100000, precision: 3 }).toFormat('$0,0.000') // logs "$100.000"
Dinero({ amount: 100, currency: 'JPY', precision: 0 }).toFormat() // logs "¥100.00"

Perhaps one of its greatest features is the chainable support for its methods, which leads to better readability and code maintenance:

Dinero({ amount: 10000, currency: 'USD' })
.add(Dinero({ amount: 20000, currency: 'USD' }))
    .divide(2)
    .percentage(50)
    .toFormat() // logs "$75.00"

Dinero.js also provides a way to set up a local or remote exchange conversion API via its convert method. You can either fetch the exchange data from an external REST API or configure a local database with a JSON file that Dinero.js can use to perform conversions.

Currency.js

Currency.js is a very small (only 1.14 kB) JavaScript library for working with currency values.

To tackle the floating-point issue we talked about, currency.js works with integers behind the scenes and ensures that decimal precision is always correct.

To install it, you just need a single command:

npm install currency.js

The library can be even less verbose than Dinero.js, by encapsulating the monetary value (whether it is a string, a decimal, a number, or a currency) into its currency() object:

currency(100).value // logs 100

The API is very clean and straightforward since it also adopts a chainable style:

currency(100)
.add(currency("$200"))
.divide(2)
.multiply(0.5) // simulates percentage
.format() // logs "$75.00"

It also accepts string parameters, such as a monetary value, with the sign, as seen above. The format() method, in turn, returns a human-friendly currency format.

However, when it comes to internationalization, currency.js defaults to the US locale. If you're willing to work with other currencies, some extra work must be done:

const USD = value => currency(value);
const BRL = value => currency(value, {
  symbol: 'R$',
  decimal: ',',
  separator: '.'
});
const JPY = value => currency(value, {
  precision: 0,
  symbol: '¥'
});

console.log(USD(110.223).format()); // logs "$110.22"
console.log(BRL(110.223).format()); // logs "R$110,22"
console.log(JPY(110.223).format()); // logs "¥110"

Currency.js is cleaner than Dinero.js in terms of verbosity, which is great. However, it doesn't have a built-in way to perform exchange conversions, so be aware of this limitation if your application needs to do so.

Numeral.js

As the library name suggests, Numeral.js is more of a general-purpose library that deals with formatting and manipulating numbers in general in JavaScript.

Although it can also manipulate currency values, it offers a very flexible API to create custom formats.

To install it, only one command is required:

npm install numeral

Its syntax is very similar to currency.js when encapsulating a monetary value into its numeral() object:

numeral(100).value() // logs 100

When it comes to formatting these values, it is closer to Dinero.js' syntax:

numeral(100).format('$0,0.00') // logs "$100.00"

Since the library has limited built-in internationalization features, you'd have to set yours up in case a new currency system is needed:

numeral.register('locale', 'es', {
  delimiters: {
    thousands: ' ',
    decimal: ','
  },
  currency: {
    symbol: ''
  }
})

numeral.locale('es')

console.log(numeral(10000).format('$0,0.00')) // logs "€10 000,00"

When it comes to the chainable pattern, numeral.js also delivers the same readability we're looking for:

const money = numeral(100)
  .add(200)
  .divide(2)
  .multiply(0.5) // simulates percentage
  .format('$0,0.00') // logs "$75.00"

Numeral.js is, by far, the most flexible library to deal with numbers on our list. Its flexibility includes the capacity to create locales and formats as you wish. However, be careful when using it since it doesn't provide a default way to calculate with zero precision for decimal numbers, for example.

Wrapping Up

In this blog post, we've explored some of the best alternatives to deal with monetary values within JavaScript, whether it is for client or backend applications. Let's recap some of the important points discussed so far:

  • If you're dealing with money, never use the floating-point primitive numbers of the language or the Number wrapper object.
  • Instead, the Intl API provided by default by your web browser is preferred. It is more flexible, secure, and smart. However, be aware of its compatibility limitations.
  • Regardless of each, if you can add a bit more weight to your app bundle, consider adopting one of the demonstrated libraries when dealing with currency calculations and/or conversions.

Finally, make sure to refer to their official docs and tests. Most of them provide great tests to help you understand the pros and cons of each library and choose the one that best suits your needs.

Get the Honeybadger newsletter

Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo
    Julio Sampaio

    Julio is responsible for all aspects of software development such as backend, frontend, and user relationship at his current company. He graduated in Analysis and System Development and is currently enrolled in a postgraduate software engineering course.

    More articles by Julio Sampaio
    An advertisement for Honeybadger that reads 'Turn your logs into events.'

    "Splunk-like querying without having to sell my kidneys? nice"

    That’s a direct quote from someone who just saw Honeybadger Insights. It’s a bit like Papertrail or DataDog—but with just the good parts and a reasonable price tag.

    Best of all, Insights logging is available on our free tier as part of a comprehensive monitoring suite including error tracking, uptime monitoring, status pages, and more.

    Start logging for FREE
    Simple 5-minute setup — No credit card required