JavaScript numbers are often inadequate to precisely represent numerical values for many applications. bignumber.js is a popular JavaScript package used in many financial and blockchain related projects to overcome this problem.

We present some of the perils of working with bignumber.js and how to mitigate them.

Arbitrary-precision arithmetic in JavaScript

JavaScript has only one number type, aptly named Number. Number supports 64-bit floating-point values and is specified in the IEEE 754 standard. This format, while good for many general purpose applications, lacks the precision needed for certain applications that are sensitive to rounding errors.

Some programming languages such as Clojure handle this problem by automatically promoting numbers that require more precision to a BigInt or Ratio type. Other languages address these needs by including arbitrary-precision number types in their standard library. In stark contrast with these other languages, JavaScript is probably one of the few mainstream languages without standard support for arbitrary-precision types.

Fortunately, the vibrant JavaScript community came up with many solutions. One of the more notable solution is bignumber.js.

How to use BigNumber.js

We are not going to cover how to use bignumber.js. For that, you can refer to its excellent documentation.

How not to use BigNumber.js

BigNumber is a great library and most of the time, it doesn’t get in your way. Because it’s so easy to use, it’s equally easy to fall under the illusion that all your calculations are now precise and safe.

For our purpose, safety here means BigNumber is able to give correct result in numerical computations, and that the serialized form of a BigNumber can be parsed by other libraries that expect a (potentially big) number without error.

Don’t Use BigNumber in Primitive Operations

When BigNumber instances are used in primitive operations such as +, >, type coercion occurs. The primitive operator will cause its operands, in our case BigNumber, to be automatically converted to a primitive type by calling the valueOf method on the object. The result of the operation may appear correct superficially, but semantically incorrect.

For example, the following is a mathematically false statement:

0.1 > 0.3 - 0.2

When expressed as BigNumber, the result we obtain is also correct:

BigNumber(0.1).isGreaterThan(BigNumber(0.3).minus(BigNumber(0.2)))
// => false

If the same operation was carried out with primitive operation, the result is incorrect:

BigNumber(0.1) > (BigNumber(0.3) - BigNumber(0.2))
// => true

It is easy to accidentally use BigNumber in primitive operations. While TypeScript catches the use of BigNumber in +, -, *, and / during compile time, the safety guaranteed is not available at runtime.

The author of bignumber.js recommends making the following change to the prototype of the default BigNumber constructor to disable type coercion, which is what makes BigNumber, a non-primitive type, behaving like a primitive type when used in the position of primitive operands.

BigNumber.prototype.valueOf = function () {
  throw Error('valueOf called!')
}

Note that this only works with BigNumber.js version 8 or above. See type coercion in the bignumber.js documentation for details.

Don’t Use toString() to get a string representation of BigNumber

Some libraries, such as ethers.js, work with arbitrary-precision numbers by parsing those numbers as string. The numbers are expected to be in non-exponential format.

For example, those libraries would expect the maximum 256-bit unsigned integer to be expressed as:

115792089237316195423570985008687907853269984665640564039457584007913129639935

One may reasonably expect the toString() method of BigNumber to give this representation. Unfortunately, this is not the case:

BigNumber(2).pow(256).minus(1).toString()
// => 1.15792089237316195423570985008687907853269984665640564039457584007913129639935e+77

The behavior of toString() mirrors that of the JavaScript primitive numbers. That is, when a BigNumber the exponent part of a BigNumber is larger than 20 or smaller than -7, the exponent notation is used.

To avoid parsing error in libraries that do not work with the exponential format, the conversion to string should be done with toString(10):

BigNumber(2).pow(256).minus(1).toString(10)
// => 115792089237316195423570985008687907853269984665640564039457584007913129639935

To avoid the exponential notation becoming a problem in practice, we can configure the BigNumber constructor to almost never convert those BigNumber instances to exponent notation by changing the EXPONENTIAL_AT. This is done by changing the EXPONENTIAL_AT setting from its default [-7, 20] to the maximum allowed range [-1e9, 1e9].

See http://mikemcl.github.io/bignumber.js/#exponential-at

TL;DR

To use BigNumber safely, create a new BigNumber constructor that prevents its instances from being used in primitive operations and converting to string in exponent format:

const SafeBigNumber = BigNumber.clone({
  EXPONENTIAL_AT: [-1e9, 1e9]
})

// Prevent use in primitive operations.
// See https://mikemcl.github.io/bignumber.js/#type-coercion
SafeBigNumber.prototype.valueOf = function() {
  throw Error('Conversion to primitive type is prohibited')
}

This create BigNumber that are compatible with the ones created by the default BigNumber constructor.

You can now use an instance of SafeBigNumber just like a regular BigNumber:

const one = new SafeBigNumber(1)
BigNumber.isBigNumber(one)          // => true

Acknowledgement

Thanks to Kyle Williams, Lio Lunesu, and Mathis Antony for providing valuable feedback.

Author

David Leung