Let’s face it: writing bug-free JavaScript code is hard.
There are some programming languages which aims to guarantee that if a program compiles, it works without runtime errors (Haskell, Elm, Idris, etc).
Avoiding runtime exceptions has always been an incredible challenge for developers, and with a weakly and dynamic typed language such as JavaScript, that’s even harder.

Where does our program typically fail?

JavaScript programs seems to have a finite set of problems, which can be solved using compilers, external libraries and adopting some best practices:

Dynamic and Weak Typing

JavaScript is a dynamic and weakly typed programming language, which means that every value we’re writing, has a runtime inferred type.
Let me explain that with an example:

main :: IO()
main = putStrLn myHello
  where myHello :: String
        myHello = "Hello World!"

In the example above, we’re just printing "Hello World!" to our console in Haskell. As you can see, we’re declaring the type of our main function (main :: IO()), and the type of myHello (myHello :: String).
In order to write our string to the console, we’re using a function called putStrLn, which (as the name suggests) only accepts strings as parameters.
That is an awesome example about how does a static and strong type system works: types are assigned by the programmer while is writing the code and not inferred during code execution. Types also can not change after they have been assigned (a variable of type String, will only accept strings, it can not change its type).

In JavaScript that is not possible, let’s see an example:

function sum(x, y) {
  return x + y;
}

sum(1, 2);   // => 3
sum(1, "1"); // => "11"

As you can see, we’re not defining any type, and we suddenly got a runtime error: we passed a string instead of a number, so our program produced a wrong output without even alerting us.

An awesome way to avoid that kind of behaviours is using a compiler which can analyze our code during compilation and alert us if there is any kind of issue with the declared types:

function sum(x : number, y : number) : number {
  return x + y;
}

sum(1, 2);   // => 3
sum(1, "1"); // => Type error during compilation!

That was an example in TypeScript, but you can also use Flow or ReasonML to achieve the same result!
I strongly believe that half of our JavaScript runtime exceptions can be solved using types.

Impure Functions

A function is pure when it follows the following rules:

  1. Returns the same value for the same arguments
  2. Its evaluation has no side effects
let x = 10;

function getX() {
  return x;
}

In the example above, we can clearly see that getX is not a pure function. It depends on a non-local variable (x) which could change its value runtime, making the function’s returning value unpredictable.

let x = 10;

function getX() {
  ++x;
  return x;
}

The example above is even harder to debug and test. Everytime we call getX, we’re causing a mutation of x, which is a side-effect. This will influence every impure function which uses x and will produce unexpected results.

const myArr = [1, 2, 3, 4, 5, 6, 7, 8, 9];

function isEven(x) {
  return x % 2 === 0;
}

function getEven(arr) {
  return arr.filter(isEven);
}

console.log(getEven(myArr)); // => [2, 4, 6, 8]

The code above represents two pure functions: isEven and getEven.
As you can see, they have no side-effects and always returns the same output given the same arguments.

Adopting pure functions will save hours of debugging and will lead to an easier way to work.
They’re also easier to combine, test, parallelize and debug... so why shouldn’t we adopt ‘em?

Unpredictable Behaviours

JavaScript was born as a scripting language for web browsers, where users can act in the most unpredictable ways.
On both browsers and servers, everything can go wrong: REST API does not respond, database does not connect successfully, user acts in an unpredictable way… how should we handle that?

Use Promises!

import db from "somedblibrary";

function getUserData(userId) {
  return new Promise((resolve, reject) => {
    
    if (!db.connection()) {
      reject("Unable to connect to db");
    }

    db.get(userId, (error, data) => {

      return error
           ? reject(error)
           : resolve(data);
    });

  });
}

As you can see, promises allows us to handle different runtime errors:

  • Database is not connecting? Reject the promise and send a friendly message to the user!
  • The query is returning an error? Once again, send a friendly error to the user!

You can use promises whenever you have to handle the possibility that an asynchronous action could fail:

getUserData(12345)
  .then((data) => `Hello ${data.name}!`)
  .catch((err) => `An error occurred: ${error}`);

Promises are not enough? Let’s take a look at Monet.js, which is an awesome monadic library containing a lot of functions that will help you handling some of the most common runtime exceptions!

Updated dependency introduces breaking changes

I can’t tell how many times this actually happened. I mean, there was also a single programmer who broke the internet by deleting an 11 lines script!

First of all, specify a fixed version for each of your dependencies:

{
  "name": "my-awesome-app",
  "version": 1.0,
  "main": "./index.js",
  "dependencies": {
    "mjn": "0.2.2"
  }
}

When you add a new dependency, forget the default "^0.0.0" version. Replace it with a fixed version number, so you’ll be sure that you’re installing the right version each time.

The biggest problem with npm is that packages can be unpublished.
It is up to you to decide if it’s worth to keep a copy of your node_modules folder.

Messy and verbose codebase

That is probably the easiest problem to solve.
A lot of programmers writes JavaScript in a imperative way, which is fine… but hard to read, debug and maintain.
A quick example:

function filterEven(arr) {

  let even = [];

  for (let i = 0; i < arr.length; i++) {
    if (arr[i] % 2 === 0) {
      even.push(arr[i]);
    }
  }

  return even;
}

filterEven is a simple function which filters out all the odd numbers off an array. The implementation above works well, but it’s hard to read, test and maintain!

const isEven     = (x)   => x % 2 === 0;
const filterEven = (arr) => arr.filter(isEven);

And what about that last solution? In just two lines of code we achieve the same result… but way easier to read, debug and maintain thanks to it declarative style of writing!

As always, if you want to write an easy maintainable and readable code, the principles you should follows are:

  • DRY (Don’t Repeat Yourself)
  • KISS (Keep it Simple Stupid)
  • Write declarative code

Want to learn more about writing declarative JavaScript? Take a look at the previous JSMonday articles: https://www.jsmonday.dev/articles

Tests

Last but not least, write tests. Please.
Tests helps you a lot to check if a new feature has some kind of side effects on the rest of your codebase.
With Jest, tests has become easier to write and you can test both your frontend and backend with just one dependency!

Adopting TDD (Test Driven Development) is an awesome method to avoid regressions and to test if your code is working as expected.
Here it is an awesome introduction to TDD in JavaScript: https://dev.to/tomekbuszewski/test-driven-development-in-javascript-olg

Some Considerations

Writing JavaScript without introducing bugs, regressions or runtime exceptions is hard.
The JS community has made an huge effort in order to introduce tools, libraries, compilers and best practices to make your application stronger, testable and maintainable.

By the way, writing runtime safe JavaScript still requires more time, which means an higher cost on your performance.
But, you know, that’s the price of a software that just works!

The cheapest work will cost you more.

Did you like this article? Consider becoming a Patron!

This article is CC0 1.0 (Public Domain) licensed.