A popular trope in science fiction movies set in space is running diagnostics whenever something goes wrong with a ship’s systems. The chief engineer might announce something like, “We’ve run a level 2 diagnostic on the thermal regulators.” This is technobabble of course, but in software development, the closest parallel to starship diagnostics we have is probably automated regression tests. Checking application logs for errors and, although less common, running integrity checks on data stores also come to mind when diagnosing tricky software bugs.

Automated test suites play a vital role in validating the correctness of code. However, they come with certain drawbacks: they exist separately from the application code they are designed to test, and they can also take a significant amount of time to execute.

Tests can easily fall out of sync with the application code if they are not maintained well. Shifting some checks from tests into the application code itself is a powerful technique that not only makes tests less brittle but also ensures runtime validation of internal application logic and data structure integrity.

Perhaps the most common method for implementing internal consistency checks is using assert statements, which throw an exception when an internal condition is violated. In my experience, assertions are most effective when applied systematically rather than in an ad hoc manner. Design by Contract, a programming approach originally introduced by Bertrand Meyer in connection with his Eiffel language offers such a system.

To illustrate the principles of Design by Contract, let’s create a simple JavaScript program that generates a data structure for building site organization charts, like the one shown below:


A sample site organization chart. A sample site organization chart.

The following code is probably as bare bones as it gets, with just three methods and virtually no checks other than what is absolutely necessary. A lot of things could go wrong with this code, some of which might not be immediately obvious:

const SiteOrganizer = function () {
    const siteMap = new Map();

    this.addSite = (site) => {
        siteMap.set(site.id, { ...site, children: new Set() });
        if (site.parentId !== null) {
            siteMap.get(site.parentId).children.add(site.id);
        }
        return this;
    };

    this.removeSite = (siteId) => {
        const site = siteMap.get(siteId);
        if (site.parentId !== null) {
            siteMap.get(site.parentId).children.delete(siteId);
        }
        siteMap.delete(siteId);
        return this;
    };

    this.getSites = () => siteMap;
};

And here’s how we add the sites in our sample organization chart:

const mySites = new SiteOrganizer()
    .addSite({ id: 1, name: 'HQ', parentId: null })
    .addSite({ id: 2, name: 'Regional Office A', parentId: 1 })
    .addSite({ id: 3, name: 'Regional Office B', parentId: 1 })
    .addSite({ id: 4, name: 'Local Site A1', parentId: 2 })
    .addSite({ id: 5, name: 'Local Site A2', parentId: 2 })
    .addSite({ id: 6, name: 'Local Site B1', parentId: 3 })
    .addSite({ id: 7, name: 'Local Site B2', parentId: 3 });

Preconditions

The first thing we will add are preconditions, which consist of assertions that enforce the assumptions a particular method makes about the input it receives. These checks explicitly document the conditions that must be true for the method to function as intended, such as ensuring that a site’s parent cannot be itself.

function assert(cond, msg = '') {
    if (!cond) {
        throw new Error(msg);
    }
}

const SiteOrganizer = function () {
    const siteMap = new Map();

    this.addSite = (site) => {
        assert(!siteMap.has(site.id), `Site ${site.name} should not have been added before.`);
        assert(site.id !== site.parentId, `Site ${site.name} cannot be its own parent.`);
        assert(site.parentId === null || siteMap.has(site.parentId), `Parent site of ${site.name} must exist.`);
        // ...
    };

    this.removeSite = (siteId) => {
        assert(siteMap.has(siteId), `Site #${siteId} must exist.`);
        assert(siteMap.get(siteId).children.size === 0, `Site #${siteId} must have no children.`);
        // ...
    };

    this.getSites = () => siteMap;
};

One can also use regular if-else blocks to test the negative versions of conditions instead of assertions. However, a major advantage of assertions is that they can be easily disabled in production, especially in performance-critical parts of the code, as not all assertions will be as fast as the ones shown above.

Postconditions

Postconditions are the guarantees a method provides to its caller when all preconditions are met. In the example below, the addSite method ensures that the site has been successfully added.

const SiteOrganizer = function () {
    const siteMap = new Map();

    this.addSite = (site) => {
        // ...

        siteMap.set(site.id, { ...site, children: new Set() });
        if (site.parentId !== null) {
            siteMap.get(site.parentId).children.add(site.id);
        }

        assert(siteMap.has(site.id), `Added site ${site.name}.`);
        assert(
            site.parentId !== null ? siteMap.get(site.parentId).children.has(site.id) : true,
            `Added site ${site.name} as sub site.`
        );
        return this;
    };

    // ...
};

It’s not always practical to test all possible outcomes, this is where test suites excel. However, using postconditions whenever possible clarifies what a method’s output should be. Even better, postconditions ensure the outcome is tested in runtime.

Class Invariants

Class invariants can be thought of as guarantees about the consistency of an object’s state. They must hold true between calls to public methods. Code within functions is allowed to temporarily break invariants, as long as they are restored before the function ends. In the example below, all the sites stored in siteMap are checked to ensure they satisfy certain conditions.

const SiteOrganizer = function () {
    const siteMap = new Map();

    this.diagnostics = function {
        function checkChildren(children) {
            let status = true;
            children.forEach((siteId) => siteMap.has(siteId) || (status = false));
            return status;
        }

        siteMap.forEach((site) => {
            assert(site.id !== site.parentId, `Site ${site.name} cannot be its own parent.`);
            assert(site.parentId === null || siteMap.has(site.parentId), `Parent site of ${site.name} must exist.`);
            assert(checkChildren(site.children), `Site ${site.name} must have valid children.`);
        });
    }

    // ...
}

Since JavaScript doesn’t natively support class invariants (or object invariants in this case), you can call the diagnostics method at the end of each public method or use a library to automate invariant checks (more on this later).

Do we really need all these checks?

The short answer is, probably not–at least not in every part of the codebase. Of the three, preconditions are the most important, and most developers implement them in some form or another. Postconditions are particularly useful in mission-critical parts of the code, ensuring that methods conform to their specifications.

As for class invariants, it might seem wasteful to check an object’s internal state every time a method is called. However, these checks can save significant time when debugging tricky state-related bugs, and they can be disabled in production code.

Automating invariant checks

“If you get the data structures and their invariants right, most of the code will write itself.” – L. Peter Deutsch

There are a few JavaScript libraries that add support for automated invariant checks, such as this one, but I haven’t found a library that works without requiring code transpilation, so I created my own: Object Diagnostics.

Using JavaScript proxies, Object Diagnostics intercepts all method calls and property accesses, including modifications and deletions. It expects a public method named diagnostics to be defined on the object, which contains the invariant checks. After each intercepted operation, it automatically calls this method.

Object Diagnostics also includes an environment-aware assert function, but you can use another library for assertions as well.

import { ObjectDiagnostics, assert } from 'object-diagnostics';

const mySites = new ObjectDiagnostics().addTo(new SiteOrganizer());

Bonus: Avoiding inadvertent mutations

Careful readers may have already noticed a fundamental flaw in our example program: its internal data structure, siteMap, though technically a private property, can still be mutated. Here’s a quick example of how:

// Set the parent id of 'HQ' to a non-existent id
mySites.getSites().get(1).parentId = 100;

When we return siteMap from getSites, JavaScript doesn’t create a copy; it shares a reference instead. This makes it possible to directly modify the object. However, as soon as another operation is performed on mySites, the diagnostics method will detect the integrity issue and throw an error.

mySites.addSite({ id: 8, name: 'Local Site B3', parentId: 3 });
// Error: Parent site of HQ must exist.

Tip: Even with invariant checks in place, it’s a good idea to use copies of objects whenever possible. A utility function like Lodash’s cloneDeep can help with this.

import { cloneDeep } from 'lodash-es';

const SiteOrganizer = function () {
    const siteMap = new Map();

    this.addSite = (site) => {
        // ...
        siteMap.set(site.id, { ...cloneDeep(site), children: new Set() });
        // ...
    }
   
   // ...
   this.getSites = () => cloneDeep(siteMap);
}

Summary

  • Preconditions verify that inputs meet the required conditions before a method executes, while postconditions ensure that the outputs are correct after the method completes.
  • Class invariants guarantee the consistency of an object’s internal state across its life cycle.
  • Runtime diagnostics complement test suites by catching integrity issues while a program runs rather than relying solely on separate tests.

Related: