JavaScript: Battle Scars

Learn from my mistakes!

Posted by Tomy Jaya on December 27, 2020

Just like many others, I used to gloss over details about JavaScript and just assumed it will behave like Java.

This lazy habit turnt out to be fine most of the times; however, there were times, it bit me hard.

The below are some of the battle scars I’ve learned the hard way:

1. Returning long (64-bit) IDs in API

Take a look at this seemingly innocuous API response shape:

class Trade {
    long tradeId;
    String tradeType;
    long amount;
    String currency;
    // ... 
}

actual JSON:

{
  "tradeId": 793548328091516918,
  "tradeType": "BUY",
  "amount": 10000,
  "currency": "SGD"
}

Somewhere in your front-end code, you will consume this by using either JSON.parse or fetch’s response.json(). And you’ll find out that the tradeId is truncated! (i.e. the last 2 digits became 00)

{
  tradeId: 793548328091516900, // TRUNCATED!!! 
  tradeType: "BUY",
  amount: 10000,
  currency: "SGD"
}

Whoops! I knew JavaScript doesn’t distinguish between floating point and integral numbers, what I didn’t know was how much of its Number storage is reserved for the integral part. It turns out here’s the allocation:

64 bit = 1 bit (sign) 
       + 11 bits (exponent) 
       + 52 bits (fraction)

793548328091516918 is order of magnitudes greater than 2^52. Hence, the truncation.

As a post-mortem, we should have used String instead of Long for the things like transactionId which doesn’t need any arithmetic operations to be performed on it.

Further Readings: How numbers are encoded in JavaScript

2. Using parseInt in map

When functional programming finally came to Java via Streams & Lambda, we all jumped to the bandwagon to write beautiful, functional, side-effect-free code. Collection-style map was all the craze.

JavaScript also lended itself nicely to functional style, with arrays having built-in map and functions as first-class objects. However, this perfectly legitimate construct in Java:

List.of("1","2","3")
    .stream()
    .map(Integer::parseInt) // method reference
    .collect(Collectors.toList());

when written in JavaScript in point-free style:

['1','2','3'].map(parseInt); // [1, NaN, NaN]

resulted in an unexpected behavior. Why? It’s because JavaScript map passes not only the value, but also the index of the item as well as the array being traversed into the mapping function. Incidentally, parseInt accepts 2 arguments: the string to be parsed and the radix. So, the above code is actually like calling:

// parse '1' with radix 0 
parseInt('1', 0, ['1','2','3']); // 1
// parse '2' with radix 1
parseInt('2', 1, ['1','2','3']); // NaN
// parse '3' with radix 2
parseInt('3', 2, ['1','2','3']); // NaN

// Gentle reminder, JavaScript just drops the extra parameter you pass in

As a post-mortem, what we should have done is to be explicit:

['1','2','3'].map(x => parseInt(x, 10));

Goodbye point-free style.. :(

3. Using Date constructor without specifying time

There were many scenarios where I only needed a date instead of a datetime. I would conveniently/ ignorantly use the JavaScript’s Date constructor passing in String in YYYY-MM-dd format. E.g.

new Date("2020-12-27")
// Sun Dec 27 2020 08:00:00 GMT+0800 
//    (Singapore Standard Time)

Notice the time is set at 8:00 am instead of midnight. Seems okay if you’re in Singapore.

However, in today’s globalized economy, it’s rarely the case that your system only needs to support one timezone and the above code is actually creating a Date object with the time set to midnight at UTC timezone. So, if you run that very same code in the US, where it’s behind the UTC, it will give you yesterday’s date (i.e. 26 Dec 2020) in the local time:

new Date("2020-12-27")
// Sat Dec 26 2020 16:00:00 GMT-0800 
//   (Pacific Standard Time) {}

new Date("2020-12-27").toDateString()
// "Sat Dec 26 2020"

Whoops! This is definitely not what you intended!

What should you do if you really need to get the date in the local time, though? Turns out, you just have to be explicit and supply the time (in ISO 8601 format) as well:

new Date("2020-12-27T00:00:00")
// Sun Dec 27 2020 00:00:00 GMT-0800 
//    (Pacific Standard Time)

Or, a better suggestion would perhaps be to use battle-tested libraries like date-fns.

4. Using JavaScript Array.prototype.sort

Many people are not aware that JavaScript’s Array’s sort does not guarantee stability!

If you come from Java background where Arrays.sort is stable, this is definitely a bummer.

To avoid “random” behavior in case elements are equal when being sorted, you will have to resort to a third party lirbary or do a preprocessing to get the index of the elements first. Later on, you can use that index lookup table to compare equal elements and get the original order they were in.

5. Not Managing Node Proceses

This one is not strictly JavaScript; instead, it’s more related to Node.js.

Unlike Tomcat with strong isolation of request threading & error handling, node process is very brittle and perhaps intentionally so. Any uncaughtException will cause the process to exit.

Hence, it’s almost never a good idea just to deploy barebone node to serve live traffic. All you need is one bad request and unhandled path to bring down your service.

Instead, you should always use a process manager (e.g. pm2) to automatically restarts your node server in case of crashes. And yes, crashing and restarting is counterintuitively the common pattern. This is because node server startup tends to be much faster than heavy weight containers like Tomcat, so the perceived downtime is minimal.

Epilogue

That’s it for now!

PS: If you’re looking for a quick read on JavaScript fundamental and gotchas as you transition from being a backend to a full-stack developer, I’d strongly recommend purchasing my book: The Minimum JavaScript You Should Know When You Code React & Redux on Amazon.