DEV Community

Cover image for Guards using invariant in JS
Nick
Nick

Posted on

Guards using invariant in JS

Intro

Have you ever heard this quote? πŸ‘‡

If you want to be the best, learn from the best.

I believe that it strongly applies to programming skills πŸ’»

So in this series, we'll gradually learn top-notch JS from famous open-source projects!

πŸ‘‰ Today's topic - guards using invariant πŸ›‘οΈ

Guard is a great way to handle errors and prevent bugs in your code.
And combined with invariant it becomes an even more powerful and versatile technique πŸ’ͺ

πŸ‘‰ First of all, what is a guard?

Guard is just validation:

  • The guard checks a specific condition
  • If the condition draws to true, the guard prevents some functionality from executing
function handleInput(input) {
  const isInvalid = !input || typeof input !== 'string';

  // Here it is!
  if (isInvalid) {
    throw new Error('Input is invalid');
  }

  return getProcessedInput(input);
}
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Okay, I'll use them!

There is an issue with using guards like this.
It forces you to repeat throw new Error in dozens of places.
So, for example, if you want to add simple logging, you have to update all guards in all places.

function handleInput(input) {
  const isInvalid = !input || typeof input !== 'string';

  if (isInvalid) {
    console.log('Input is invalid'); // <- add this in all places  
    throw new Error('Input is invalid');
  }

  return getProcessedInput(input);
}
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ How to do it like a top-performer?

React developers faced the same issue and added special invariant abstraction to resolve it.

It does the same thing while preserving the DRY principle.

function handleInput(input) {
  const isValid = input && typeof input === 'string';

  invariant(isValid, 'Input is invalid');

  return getProcessedInput(input);
}
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ How does invariant work under the hood?

The most popular JS invariant implementation takes multiple arguments:

  • condition, that defines whether an error needs to be thrown
  • format, simply put, the error message
  • six optional arguments to be placed instead of %s inside the format
invariant(
  1 > 2, 
  'Hello from error. %s %s %s %s %s %s',
  1, 2, 3, 4, 5, 6
);

// Results to
// > Uncaught Error: Hello from error. 1 2 3 4 5 6
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Let's re-implement it!

As always, let's re-create invariant ourselves to get an in-depth understanding of its inner workings.

Our version uses modern ES6+ syntax and supports an indefinite number of optional arguments.

const invariant = (condition, format, ...args) => {
  if (!condition) {
    if (!format) {
      throw new Error('General error occured.');
    }

    let i = 0;

    throw new Error(format.replace(/%s/g, () => args[i++]));
  }
};
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Implement it yourself!

Go to my CodeSandbox and try to implement the invariant function, based on what we just learned.


P.S. Follow me on Twitter for more content like this!

Oldest comments (12)

Collapse
 
costinmanda profile image
Costin Manda

I think invariant is over engineered. In Javascript one can use || to execute something based on a condition. Then the function translates to isValid || throwit(...)

Collapse
 
fromaline profile image
Nick • Edited

I agree with you for the most part.
It seems like invariant is one of these weird utilities, like is-odd/is-even.

Collapse
 
costinmanda profile image
Costin Manda

Sometimes you want to be more specific. Instead of x % 2 == 0 you say isEven(x) and now you can read the code better. Of course, then you might want to get creative and check that the parameter of isEven is an integer number first and then others get creative and check is something is an integer with if (isEven(x) || isOdd(x))... =))

In the case you described it seems that you are going in the other direction. In fact having it written as if (!condition) throwSomething(...) is even more readable. I actually can't wait for having some metadata info in my language so I can do something like assertNotNull(x) with something like

function assertNotNull(x) {
  if (x===null || x===undefined) {
    throw new NullAssertError(metadata.parameters[0].name+' cannot be null');
  }
}
Enter fullscreen mode Exit fullscreen mode

Instead of going for a superfluous invariant function, I would go for specific and readable stuff like assertDateLargerThan and assertCustomerIsSatisfied and stuff like that. Surely you see I am a .NET developer, but that's my two cents :)

My conclusion is that I want to describe what I want to do, then make the code understand what I meant, rather than the other way around.

Thread Thread
 
fromaline profile image
Nick • Edited

I kinda get your point! You think, that invariant is too generic and would prefer having a specific function for each use case to make code more readable.

It seems like a valid point to me, especially if we talk about the backend, where code doesn't need to be sent to the client and parsed on its side.

Thanks for adding value to the discussion πŸ™πŸ»
It's so cool to communicate with experienced devs!

Collapse
 
shuckster profile image
Conan

If you use TypeScript, a useful version of invariant is tiny-invariant:

import invariant from 'tiny-invariant';

const title: any = getValueFromSomewhereUntrusted();
// At this point TS will type 'title' as 'any'

invariant(typeof title === 'string');
// At this point TS will type 'title' as 'string'
Enter fullscreen mode Exit fullscreen mode

Because of the use of asserts at this source-line you're dropping a hint to the type-engine about what you've called invariant on.

Collapse
 
fromaline profile image
Nick

Yeah, you're right. It's so simple yet useful difference.
Thanks for adding value!

Collapse
 
fromaline profile image
Nick

Wow, you're right, it's the same abstraction!
C is so influential, I regret not learning it in college.

Collapse
 
assertnotnull profile image
Patrice Gauthier

Dropping a different idea here but using Either in JS or TS with that last link having a good description. It also solves the problem of error bubbling and not knowing if a function will throw an error and you have to handle it since the moment you have an Either, you know there's an error state.

Collapse
 
mrdulin profile image
official_dulin • Edited

What's the difference between invariants and assert?

Collapse
 
fromaline profile image
Nick

As far as I see, assert provides a predefined set of assertions for verifying invariants with predefined error messages.

invariant, in turn, lets you verify arbitrary conditions and throw user-defined error messages.

Collapse
 
copperfox777 profile image
copperfox777

The whole article about reinventing functions?

Collapse
 
fromaline profile image
Nick

Don’t get it. Why?