Defensive Modern Javascipt

13 Jul 2018

At work I’ve been recently exposed to a large and complicated React application written in ES6. Of all the refactoring I’ve been undertaking, I have noticed that there was not much defensiveness built into the classes and functions.

By defensive programming, I mean writing the code in such a way that expects it to fail, and gracefully handles common and recurring problems in JS. You do this so your application will continue regardless of any unforeseen problems that occur.

In this post, I’ll go through some of the things I’ve found that have worked.

Turn off sloppy mode

"use strict";

For backward compatablity with ES4 and back, ES5 added a ‘strict mode’. By default, ‘sloppy mode’ will still allow a lot of stupid stuff to happen without telling you.

For example, accidentally creating a global variable due to a typo will trigger an error

let thing = 12
thang = 24 // Triggers error only in strict mode

Read more about strict mode here

Encapsulation

Hiding internal variables outside of the module that’s exported, so that the users of the module can’t interrupt the internal workings. Due to all methods and data in JS being public on the frontend, you still shouldn’t add sensitive information into public JS files (like shared secrets). Leave them on the server-side so they can’t be found in client-side code.

// ./lib/dogs.js
const dogList = new WeakMap()

class Dogs {
  constructor(size) {
    dogList.set(this, ["Jack Russel", "Collie", "Golden Retriever"])
  }

  getDogs() {
    return dogList.get(this)
  }
  
  addDog(dog) { 
    dogList.get(this).push(dog)
  }
}

export default Dogs;

// --

const dogs = new Dogs()
dogs.addDog("Shiba Inu")
console.log(dogs.getDogs())
=> ["Jack Russel", "Collie", "Golden Retriever", "Shiba Inu"]
console.log(dogs.dogsList)
=> Error

Protect functions from bad data

You’ll have methods in your app that are guaranteed to only receive one variable type, so enforce this as a requirement

var adder = {
  from: 1,

  add: (number) => {
    if (!number || typeof number !== 'number' || number < 0) {
      throw('not a number')
    }
    return adder.from + number
  }
}

console.log(adder.add(3))
=> 4

console.log(adder.add("toast")
=> uncaught exception: not a number

TypeScript

TypeScript goes one step further, by strong enforcing input types on functions

function hiThere(name: string) {
    return "Hi there, " + name;
}

let name = [0, 1];

console.log(hiThere(name))

During the compilation process it’d give you an error

error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.

This phase happens before your code is run, so with TypeScript you’ll get a lot more certainty about not passing rubbish data to functions.

Read more about TypeScript here

Immutable bindings

One of the biggest changes between EC5 to EC6+ was the addition of the const. The keyword has caused a lot of confusion (me included) in the past.

const a = {}
a.b = "toast"
console.log(a.b);
=> "toast"

a is mutable but the binding of a is immutable, meaning that it can’t be reassigned further on, for instance:

const a = {}
a = "toast"
=> TypeError: invalid assignment to const 'a'

const a = {a: "c"}
=> SyntaxError: redeclaration of const a

const creates a read-only reference to the variable, but the variable itself is still mutable.

You should use the const variable by default when assigning variables, as this will provide some free benefits. Once we we can trust that our immutable reference hasn’t changed, we can be assured that the variable has been changed within another function or scope, a huge bugbear of old style JS.

Immutable variables

You can make variables immutable in a few ways, the easiest is to use modules. With ES6, modules are automatically in strict mode.

This would be best used for default state, like placeholder text in a field or date format string.

Place these in a folder of their own in webpack to essentially freeze them

webpack/consts/date_formats.js

export const iso = "YYYY-MM-DD"
export const long = "dddd Do MMMM YYYY"

Or if you’re within a function and wish to make an object immutable, you can freeze it

use "strict";

const thing = Object.freeze({
  a: "a"
})
a.b = "b"
// Throws an error because of strict mode, ignored in sloppy mode

async and await over callback hell

ES8 allows you to use the async and await keywords, any one that uses Node.js will be familiar with these.

The handling of external resources and data is the most likely cause of problems, errors and bugs

We’ll go on to this a little bit more with establishing a contract, but for now we can look at the low-level code interacting with the external sources of data.

Let’s use a jQuery.ajax() as a common example, before we’d need to use callbacks to notify our code of the new data

$.ajax({
  url: "test.html",
})
.done(function(html) {
  // do your thing
});

The main problem with this is that the .done() method become larger and larger, with function spinning off of it in all directions, handling the data coming in. What if the data isn’t there, or malformed? All of this logic must be in callbacks made from jQuery.

Instead, consider using async Fetch API.

async function fetchData (url) {
  let response = await fetch(url)
  if (response.ok) {
    return await response.json()
  }
  // Error handling
}

const url = "http://api.twitter.com"
fetchData(url)
  .then(data => console.log(data))

Reduce third-party modules

Your application will interact with third-party libraries, it’s inevitable. Some of these are more important that others.

When looking to refactor code, or to rewrite a component, start with third-party modules that are only used sparingly, or ones that are just proxies for standard libraries.

  1. Reduce your reliance on libraries, especially ones only used a few times
  2. Look for web standards ways of doing what you need, instead of using a library
  3. Keep the modules you keep up-to-date

Get rid of jQuery

You don’t need it. jQuery is the library of 2009, we’ve moved on a lot since it was thought up and released.

The excellent and eternal You Might Not Need jQuery shows how not to use it.

You might not consider jQuery a problem in your application, but modern Web APIs often superceed it’s methods, and it opens up your application to XSS and common attack vectors - just look up the jQuery version your using and I’ll bet you there’s ways to exploit it.

Let’s all remove jQuery from our applications and treat it as a legacy library - it’s day has passed.

More observers, fewer events

A key place for memory leaks are performance issues in ‘old’ JS was with endless onThing events. Reactive design has helped with this but we can go a step further with encapsulating event handlers within Observers.

Take the onResize event in an old application

function doThing(e) {
  console.log("changed")
}

window.addEventListener('resize', doThing);

If we intead treat the window.addEventListener method as untrusted, then we can maybe treat it and abstract it’s calls away.

const resize = new Observable((o) => {

  function onResize() {
    o.next({ window.innerHeight, window.innerWidth })
  }

  window.addEventListener("resize", onResize)

  // Tidy up the observer handler
  return () => {
    window.removeEventListener("resize", onResize);
  }
})

var windowSizeHandler = {
  next: (value) {
    // Do stuff with the window size
    console.log(value)
  }
}

var resizeSubscription = resize.observe(windowSizeHandler);

// Clean up the handlers & observers
resizeSubscription.unsubscribe();

These Observer calls are made on the idle thread of the browser, whereas event listeners are on the main thread, stopping and halting the execution of other critical things.

Establishing a strong contract

This is the hardest task, and hands waving about in the air task - but one that will produce a stronger foundation that your app sits on.

This is not just an ES7 defensive measure, but more generally for all programming between components.

Most of programming between components and third parties is about making a contract that the two can agree on.

The contract between backend and frontend is your API, usually as AJAX requests and data in the DOM. This is not just an ES6 task - any components that cross the backend/frontend frontier are establishing or using a contract between the two.

These are the most common points of failure in your application, and as such, need to be dealt with care (and suspicion). Test drive the behaviour of the frontend using Jest or Cucumber, so that you can refactor and still be sure that the code works.

Given I am on the homepage
When I type "Beach Ball" into the search bar
Then I see results for "Beach Ball" from the Search API
Then I see related results for "Beach" from the Suggestion API
Given I am a logged in user
Given I am on the user account page
When I change my email address
When I click "Update"
Then I should see my email address change

You might be creating and maintaining both sides of this contract if your a full stack dev, that will help!

It’s important that both sides of the contract, frontend developer and backend developer agree a strong, well-defined and versioned contract, perhaps that’s a well documented API, so that when it reaches the other side, we already have strong domain knowledge about what’s going on.

Building this contract between the parties builds trust that your application works as it should, and that will help if and when things change on either side.

Exception handling

Don’t use exceptions as control flow. Expect them to happen in your application, but don’t handle them directly unless you absolutely have to.

Instead, handle why they are happening, for instance a variable reference being nil. If you try {} catch {} everything you’ll miss errors you need to see and end up causing more problems.

Sign up for an account with Airbake.io, add their window exception catching and solve the ones that look important as they come in.

Future: Optional chaining

Chances are you’ve seen code like this

if (animal && animal.legs && animal.legs.length == 4) {
  console.log("dog")
}

Well, there’s a proposal in ES9 for optional chaining, syntactic sugar that makes this look much nicer

if (animal?.legs?.length == 4) {
  console.log("dog")
}

This (in my opinion) hides the problem, and doesn’t address the problems the code actually has and would probably be seen as an anti-pattern.