Productivity is the result of a commitment to excellence, intelligent planning, and focused effort.

The Power of Ten. Rules for Developing Safety-Critical Code in JavaScript

Cover Image for The Power of Ten. Rules for Developing Safety-Critical Code in JavaScript
George Crisan
George Crisan
Posted on:

Introduction

To make this clear from the start, this article has my opinion on how to translate to JavaScript The Power of 10: Rules for Developing Safety-Critical Code, which was written with 'C' in mind. In safety-critical software systems, such as medical devices, aviation industry etc, reliability has primacy. Here’s how Gerard J. Holzmann's Power of Ten can be applied to JavaScript.

1. Avoid complex flow constructs

JavaScript can be prone to complex control flow constructs like deep nested ternary operators, and excessive callbacks. Keep flow structures simple.

I will create two bad examples of the same functionality. First abusing ternary operators and the second making it over complicated. This code is hard to read and difficult to maintain.

// Example 1
const calculateDiscount = (product) =>
  product
    ? product.onSale
      ? product.discount > 0
        ? product.price - product.discount
        : product.price
      : product.price
    : 0;

// Example 2
const calculateDiscount = (product) => {
  if (product) {
    if (product.onSale) {
      if (product.discount > 0) {
        return product.price - product.discount;
      } else {
        return product.price;
      }
    } else {
      return product.price;
    }
  } else {
    return 0;
  }
}

Let's improve it with early returns, avoiding deep nesting, making it flat and easy to follow.

const calculateDiscount = (product) => {
  if (!product) {
    return 0;
  }

  if (!product.onSale || product.discount <= 0) {
    return product.price;
  }

  return product.price - product.discount;
}

2. All loops must have fixed bounds

Loops should always have a statically known limit, especially when dealing with large datasets. Avoid infinite loops or dynamic bounds that can lead to performance issues or unexpected behavior. When possible, use higher order functions like forEach(), map() etc Fixed bounds can help avoid scenarios like accidental infinite loops, especially when dealing with dynamic data sets.

3. Avoid dynamic memory allocation (minimize object creation and promote referencing)

While JavaScript is garbage-collected, excessive object creation (like within loops or recursive functions) can lead to unpredictable behavior or memory bloat. Minimize unnecessary object allocation and reuse objects where possible. This is a naive example but good enough to prove the point.

for (let i = 0; i < 10000; i++) {
  // heavy object created on each iteration
  const tempArray = new Array(1000).fill(0);
  process(tempArray);
}

Better by reuseing memory instead of recreating:

// heavy object created outside and referenced
const tempArray = new Array(1000).fill(0);

for (let i = 0; i < 10000; i++) {
  process(tempArray);
}

You may argue that JS has a garbage collector to take care of that. Well, even though GC will clean it up, if you create thousands of arrays per second, GC kicks in often with a performance cost.

4. Limit Functions to a single printed page

Functions should remain small and focused on a single responsibility. Avoid functions that stretch over multiple lines or handle multiple tasks at once. Refactor long functions into smaller, more manageable ones. I find 50 to 90 lines of code acceptable.

5. Use Assertions to validate critical conditions

Using assertions is not a common practice in JS ecosystem. The closest thing is using unit tests. Most JavaScript developers rely on testing frameworks like Jest and Mocha, and I think that should suffice to satisfy the fifth rule.

6. Restrict the scope of variables

Always limit variable scope to the smallest necessary context. Avoid global variables as they can lead to side effects and make code harder to reason about. Thanks to ES6, let and const makes this very easy. Using let and const instead of var is not only for limiting scope but also for reducing potential issues like variable hoisting.

total = 0; // global!
const add = (x) => {
  total += x;
};

vs

const add = (x) => {
  let total = 0;
  total += x;
  return total;
};

7. Check the return value of all Functions

Always check the return value of functions to ensure they have executed successfully, especially for functions that interact with external systems or libraries, for example API calls.

8. Use constants instead of magic numbers

Avoid using raw values like 60 or 3.14 in the code. Replace them with named constants to make the code more readable and maintainable.

9. Provide a default case in every wwitch statement

Always provide a default case in a switch statement to handle unexpected or edge cases. This prevents bugs from being silently ignored.

10. Avoid deprecated or non-standard Browser APIs

JavaScript has several deprecated or non-standard browser specific features that should be avoided to ensure cross-browser compatibility, security, and long-term maintainability. These features are often replaced by better, more secure alternatives, and relying on them can cause issues in modern web development. Always use standardized methods provided by the latest ECMAScript specification and modern web APIs.

Example:

// Avoid using `attachEvent`, which is deprecated
element.attachEvent("onclick", function () {
  /* event handler */
});

// Avoid using `document.all`, which is not part of the standard DOM API
const element = document.all["someElement"];

// Avoid vendor-specific prefixes
window.webkitRequestAnimationFrame(function () {
  /* animation */
});

This is my interpretation of Gerard J. Holzmann's Power of Ten rules. Thank you for following so far.