Invariant - a helpful JavaScript pattern

I've been reading through the Remix documentation lately (which, by the way, is awesome), and my eye caught a handy little library they used called tiny-invariant.

I'm not going to talk about a particular implementation here (while it's great!), but rather about the idea, or a design pattern if you will.

What I like about it is that it's super simple and elegant, and it greatly improves the readability of your code.

Disclaimer: I will stick with invariant (similarly to what it was named in React, and in libraries) throughout this article. But keep in mind that, as many people pointed out, a proper name for the pattern is assert (see Node's assert for example).

Let's start with and example.

Imagine an API call handler that takes a path parameter id and renders a book loaded from a database.

// GET /api/v1/books/{id}
async function getBook(params) {
  const { id } = params
  const book = await findBook(id)
  return Response.ok(JSON.stringify(book))
}

We expect id to be numeric, but nothing stops our inventive user from entering /api/v1/books/foobar into the address bar.

Assuming that our framework can handle Errors properly (for example, by returning a 400), we add some sanity checks.

async function getBook(params) {
  const { id } = params;
  if (id) {                       // not an empty string
    const idAsInt = parseInt(id);
    if (!isNaN(idAsInt)) {        // is it a number?
      const book = await findBook(idAsInt);
      return Response.ok(JSON.stringify(book));
    } else {
      throw Error("Id must be numeric");
    }
  } else {
    throw Error("Id must be present");
  }
}

It works, but it looks hideous!

Too many indents and if/else blocks. It looks cramped, and the success case is buried somewhere in the middle.

We can make it much better if we exit early. This way, we'll get rid of indents and a cognitive overload.

async function getBook(params) {
  const { id } = params;
  if (!id) {
    throw Error("Id must be present");
  }

  const idAsInt = parseInt(id);
  if (isNaN(idAsInt)) {
    throw Error("Id must be numeric");
  }

  const book = await findBook(idAsInt);
  return Response.ok(JSON.stringify(book));
}

Much better, isn't it?

And now it's easier to see a pattern here.

Every time we assert some condition, and if it doesn't hold, we throw an error. It's easy now to extract it into a function.

async function getBook(params) {
  const { id } = params;
  invariant(!!id, "Id must be present")

  const idAsInt = parseInt(id);
  invariant(!isNaN(idAsInt), "Id must be numeric")

  const book = await findBook(idAsInt);
  return Response.ok(JSON.stringify(book));
}

// utils.ts
function invariant(cond, msg) {
  if (!cond) {
    throw Error(msg)
  }
}

Clean and simple.

Naturally, it works with TypeScript.

const { id } = params;
id // type: string | undefined

invariant(typeof id === "string", "Expected a string");
// type: string

We now have a runtime check plus the compile-time check for types with a simple function.

Where to go next?

The tiny-invariant is just about 30 lines of code, and it improves on this idea a bit. It adds a prefix to your message and makes it possible to optimize out the message at compile time (to save some bytes) - so don't hesitate to read the code.

🔥 100+ questions with answers
🔥 50+ exercises with solutions
🔥 ECMAScript 2023
🔥 PDF & ePUB