Some common misunderstandings in JavaScript
Jun 26, 2019 11:47 ยท 744 words ยท 4 minute read
Disclaimer: This article talks about how some parts of the JavaScript language can be misunderstood by developers leading to an unexpected results.
Do you know why the following code will end up charging $5 to the user? ๐จ
const discount = (amount, quantity) => amount - quantity;
let amount = 1.5
amount = discount(amount,1.2) // 1.5 - 1.2 = 0.3
amount = discount(amount,0.3) // 0.3 - 0.3 = 0
return parseInt(amount) // 0
The discount
function subtracts the discount amount from the total amount we have to pay.
The user is lucky as we’ll discount the total amount he had to pay. However, somehow, we end up charging $5 to him. ๐
This unexpected behavior comes from two different issues tied to the language.
The first one appears when we subtract 1.5 - 1.2
, the expected result would be 0.3
but the real result is 0.30000000000000004
.
This small error, known as rounding error, comes from how JavaScript processes the floating-point. You can read more in this excellent article of Brian Rinaldi.
Because of this small issue, when we subtract, 0.3
we wind up with 0.00000000000000004
instead of 0
, and here is when the second issue appears.
What we would expect from parseInt
is to leave the value untouched, however, this is not happening.
Once you execute parseInt(1.5-1.2-0.3)
the result will be 5. ๐
Let’s delve in what’s happening here.
The parseInt
function is doing one thing under the hood. It transforms the number to its string representation before parsing it.
The funny thing here is that given 7 decimals or more the string representation changes from 0.0000001
to 1e-7
and, from here, everything gets messed up.
The parseInt
function was designed to handle not only decimals but octal and hexadecimal values too. In order to do that it returns the first number of the string instead of failing.
The operation(1.5-1.2-0.3)
it’s displayed as 5.551115123125783e-17
. If we parse this to an integer it will consider only the first number of the string 5.551115123125783
and it will remove all decimals.
In that way we’ll end up with 5
, which is exactly what we charged to our customer.
Let’s see another example:
const sum = (a,b) => a + b;
const cutDecimals = (num) => num.toFixed(2)
const sumItemPrices = itemPrice => parseFloat(itemPrice.map(cutDecimals).reduce(sum,0));
const itemPrices = [1,3,1.345]
return sumItemPrices(itemPrices)
The expected output for this function should be 1+3+1.54 = 5.34
but instead the value returned it’s 1.003
.
This particular issue is locaten in the toFixed
funtion. This function is meant to be a decorative function returning a string
value instead of a number
(a bit counterintuitive don’t you think?).
Because of that when we cutDecimals
of our array of numbers [1,3,1.345]
it get’s transformed into an array of strings like this ["1","3","1.345"]
therefore when we apply the sumation function to every single value instead of summing we are concatenating the values ending up with the following string "01.003.001.34"
. If we try to parseFloat
this string it will stop on the second dot as will consider the rest as unvalid characters and it will return 1.003
.
Now let’s talk a bit about types. Let’s see this function:
const isNumber = num => typeof num === "number";
They look reasonable and correct but it’s not, in fact, have an edge case in which don’t behave as expected.
isNumber(1) // -> true, it's ok
isNumber("x") // -> false, it's ok
isNumber(parseInt("x")) // -> true, ???
What’s happening here is that parseInt("x")
returns NaN
(not a number) and the type of “not a number” is a number, which again can be kind of confusing.
And you can find more thinks in the language that can go against your common sense like these ones:
typeof null // -> "object"
typeof [] // -> "object"
isNaN([]) // -> false
undefined == null // -> true
As we can see, in javascript, a very “innocent” code can became a disaster if we don’t really know the insights of what we are doing and even if we know it’s easy to make mistakes.
For that reason we created a small open-source library called keyu which pretends to relieve developers from this hassle. For now it’s a very small and humble library but with your collaboration can be a good reference for the rest.
If you are interested to contribute you can find the source code here
If you feel the pain, contribute to it. If you like it, share it and if you don’t, please, criticize it.