06. Null Object Pattern

tl;dr October 30, 2019

Are your functions overly distrustful? We’ll see just how the Null Object Pattern can restore a culture of trust and cut down flow control bugs on today’s episode of TL;DR, the JavaScript codecast series that teaches working web developers to craft exceptional software in 5 minutes a week.

Transcript

We just hit 100 subscribers, and I want to thank you so much for watching, sharing and subscribing! This series is based on problems faced in real-world consulting projects, so each episode is designed to teach powerful patterns that save you and your team time. But that takes a lot of time! Each 5–7 minute episode takes around 30 hours from inception to release.

So I want to ask you to consider supporting me on Patreon so I can keep crafting great content that helps you craft exceptional code. Think of it as a pay-what-you-like screencast subscription.

Alright, on to today’s pattern.

When each line of code has to defend against previous lines, we’re left with a tangle of branching and flow control constructs, like if…else and early returns. This is especially tricky for error handling code, which can turn an otherwise docile function into a flow control nightmare.

Today we’re working on authentication middleware that’s taken from a chapter in my book, Functional Design Patterns for Express.js. If you’re hopping into a Node backend and have loved the design-oriented approach we take on TL;DR, I encourage you to check it out.

We’re making authenticated HTTP requests to an Express backend and supplying a special token called a JSON Web Token. If you’re new to JWTs, think of them as the picture page of a passport: they encode details about the user, and are securely signed so the backend knows they’re authentic.

makeRequestWithToken('g00d_t0k3n');
// => '✅ 200: Welcome to the backend.'
makeRequestWithToken('3mpt1_t0k3n');
// => '🚫 401: Bad token.'
makeRequestWithToken('0ld_t0k3n');
// => 💥 TokenExpiredError: jwt expired
makeRequestWithToken('[email protected]_t0k3n');
// => 💥 JsonWebTokenError: jwt malformed

As long as the token is valid, the backend lets us use any of its APIs. But if the token is missing some information, has expired, or has been tampered with, the backend halts the request in its tracks.

The function responsible for this guard behavior is a middleware function called checkToken():

let checkToken = (req, res, next) => {
  let payload = jwt.verify(req.token, 's3cr3t');

  if (payload && payload.user) {
    req.user = payload.user;
  } else {
    res.status(401).send('Bad token.');
    return;
  }

  next();
};

It tries to decode the contents of the JSON Web Token, called the payload. If the token is successfully decoded, it stores the user information on the request object and invokes next() to continue. But if the token is bad, it halts the request and immediately responds with a 401 Unauthorized status code.

But a lot of other things can go wrong. A client could supply an expired token, or they might tamper with it; in either case, the jwt.verify() function throws an exception. Right now, the checkToken() function is completely oblivious to these potential errors.

We should never allow a known exception to go uncaught, otherwise the backend’s response will just hang. So instead, we need to catch any JWT-related errors and respond with a 401 status code.

 let checkToken = (req, res, next) => {
-  let payload = jwt.verify(req.token, 's3cr3t');
+  let payload;
+  try {
+    payload = jwt.verify(req.token, 's3cr3t');
+  } catch (error) {
+    /* Suppress the error */
+  }

   if (payload && payload.user) {
     ...
 };

To do that, we can wrap try…catch around the verify() call. But as we learned in the last two episodes, an unqualified catch is almost always a bug. We must only catch error types we intend to handle. We’ll use an if…else statement to rethrow the error if it isn’t a TokenExpiredError or JsonWebTokenError.

let checkToken = (req, res, next) => {
  let payload;
  try {
    payload = jwt.verify(req.token, 's3cr3t');
  } catch (error) {
    if (error instanceof TokenExpiredError
     || error instanceof JsonWebTokenError) {
      /* Suppress the error */
    } else {
      throw error;
    }
  }

  if (payload && payload.user) {
    req.user = payload.user;
  } else {
    res.status(401).send('Bad token.');
    return;
  }

  next();
};
makeRequestWithToken('g00d_t0k3n');
// => '✅ 200: Welcome to the backend.'
makeRequestWithToken('3mpt1_t0k3n');
// => '🚫 401: Bad token.'
makeRequestWithToken('0ld_t0k3n');
// => '🚫 401: Bad token.'
makeRequestWithToken('[email protected]_t0k3n');
// => '🚫 401: Bad token.'

This is the correct way to handle all these edge cases, but now checkToken() is swimming in flow control constructs: early returns, try…catch, throw, and an unhealthy dose of if…else statements too. And sadly, this style is typical of most popular middleware libraries.

Each line of code is constantly on guard, as though it can’t trust the lines before it. So how do we nuke these flow control constructs?

Last episode we derived a helper called swallow() that could help. swallow() is a higher-order function that runs some code that could potentially blow up. If it does, it suppresses the error and instead returns the result of another function.

let swallow = (type) => (fail) => (fn) => (...args) => {
  try {
    return fn(...args);
  } catch (error) {
    if (!(error instanceof type)) { throw error; }
    return fail(error);
  }
};

let safeFindBlog = swallow(NotFound)(
  () => 'Missing blog'
)(unsafeFindBlog);

unsafeFindBlog({ id: 5 });
// => { title: 'I 😍 JS' }
unsafeFindBlog({ id: 100 });
// => 💥 NotFound
safeFindBlog({ id: 100 });
// => 'Missing blog'

Let’s try using swallow() in place of the try…catch and if…else statements. If jwt.verify() throws a TokenExpiredError, we’ll catch it and instead return null to make it mirror the old behavior.

let checkToken = (req, res, next) => {
  let payload =
    swallow(TokenExpiredError)(
      () => null
    )(
      () => jwt.verify(req.token, 's3cr3t')
    )();

  if (payload && payload.user) {
    ...
};

Since swallow() is a higher-order function, we can also catch a JsonWebTokenError by composing it with another swallow().

 let checkToken = (req, res, next) => {
   let payload =
+    swallow(JsonWebTokenError)(
+      () => null
+    )(
       swallow(TokenExpiredError)(
         () => null
       )(
         () => jwt.verify(req.token, 's3cr3t')
+      )
     )();

   if (payload && payload.user) {
     ...
 };

This is horrible to read, but it behaves correctly and removed several flow control constructs. What about the remaining conditionals? It would help if we could go ahead and destructure the payload’s user property. Then the following code could be less defensive about the shape of payload.

Well if a TokenExpiredError is thrown, swallow() will return null, which isn’t an object and can’t be destructured. So what if instead of returning null, we returned a benign value that has the shape of a valid payload, such as an object with a user property? Then even if an exception is thrown, we can be sure that the payload will have the right shape.

 let checkToken = (req, res, next) => {
-  let payload =
+  let { user } =
     swallow(JsonWebTokenError)(
-      () => null
+      () => ({ user: null })
     )(
       swallow(TokenExpiredError)(
-        () => null
+        () => ({ user: null })
       )(
         () => jwt.verify(req.token, 's3cr3t')
       )
     )();

-  if (payload && payload.user) {
+  if (user) {
-    req.user = payload.user;
+    req.user = user;
   } else {
     ...
 };

By substituting a benign value as early as possible, we don’t have to be defensive later on. In Object-Oriented Programming, this benign value is called a Null Object. It’s often a subclass of the expected object type, and should respond to the same messages.

class User {
  constructor({ id, name, email }) {
    this.name = name;
    this.email = email;
    this.id = id || generateId();
  }
}

class NullUser extends User {
  constructor() {
    super({
      id: '00000000',
      name: 'NullUser',
      email: '[email protected]'
    });
  }
}

Since we’re taking a more functional approach, we won’t create a Null Object class, but we can still lift this Null Object into a variable called nullPayload to better communicate intent.

let nullPayload = { user: null };

I use this pattern so often, I like to create a utility called rescueWith() that behaves exactly like swallow(), except that we don’t need the extra function wrapping around the nullPayload.

let rescueWith = (type) => (fallback) =>
  swallow(type)(() => fallback);

let checkToken = (req, res, next) => {
  let { user } =
    rescueWith(JsonWebTokenError)(nullPayload)(
      rescueWith(TokenExpiredError)(nullPayload)(
        () => jwt.verify(req.token, 's3cr3t')
      )
    )();

  if (user) {
    ...
};

That helps cut down the syntactic noise, and once we move the arguments for jwt.verify() to the end:

let checkToken = (req, res, next) => {
  let { user } =
    rescueWith(JsonWebTokenError)(nullPayload)(
      rescueWith(TokenExpiredError)(nullPayload)(
        jwt.verify
      )
    )(req.token, 's3cr3t');

  if (user) {
    ...

We now see the entire function can be extracted from checkToken() altogether! Let’s call it safeVerifyJWT since it works exactly like jwt.verify() but just replaces errors with a safe value.

let safeVerifyJWT =
  rescueWith(JsonWebTokenError)(nullPayload)(
    rescueWith(TokenExpiredError)(nullPayload)(
      jwt.verify
    )
  );

let checkToken = (req, res, next) => {
  let { user } = safeVerifyJWT(req.token, 's3cr3t');

  if (user) {
    ...

Finally, let’s whip out our compose() helper to remove the nesting.

let safeVerifyJWT = compose(
  rescueWith(JsonWebTokenError)(nullPayload),
  rescueWith(TokenExpiredError)(nullPayload),
)(jwt.verify);

This refactor has helped us discover the boundary we should have seen all along: all that try…catch and if…else nonsense was just about making a version of jwt.verify() that behaved a little differently — just the sort of thing higher-order functions do so well.

And now checkToken() is back to focusing on the naive happy path. With all the noise out of the way, we can confidently reason that next() will only be called if there’s a user, so we can move it into the if clause and eliminate the early return in the else. This code now has zero flow control constructs!

 let checkToken = (req, res, next) => {
   let { user } = safeVerifyJWT(req.token, 's3cr3t');

   if (user) {
     req.user = user;
+    next();
   } else {
     res.status(401).send('Bad token.');
-    return;
   }

-  next();
 };

Optionally, we can even rewrite the remaining if…else statement into a ternary expression to prohibit any flow control constructs at all. But whether or not you use the ternary, the final checkToken() function reads nicely thanks to small, well-behaved functions and a predictable flow.

let nullPayload = { user: null };

let safeVerifyJWT = compose(
  rescueWith(JsonWebTokenError)(nullPayload),
  rescueWith(TokenExpiredError)(nullPayload),
)(jwt.verify);

let checkToken = (req, res, next) => {
  let { user } = safeVerifyJWT(req.token, 's3cr3t');

  return user
    ? (req.user = user, next())
    : res.status(401).send('Bad token.');
};

We’ve been building up to this refactor for a few episodes, but by letting things get ugly instead of skipping directly to rescueWith(), we saw how composition always wins in the end — even if the process seems to produce more code.

And that journey helped us identify and solve the underlying problem: trust. Each line of code was defensive because it couldn’t safely trust the results of lines before it. With this variation of the Null Object Pattern, we replaced edge cases with benign values. Once we did that, the boundaries became detangled so we could extract a safe version of jwt.verify().

Trust is a powerful refactoring tool. Today, look for try…catch statements, followed by if…else statements, and use the Null Object Pattern and rescueWith() to restore a culture of trust.

That’s it for today! If you loved today’s episode, please consider supporting the channel on Patreon. Want to keep leveling up your craft? Don’t forget to subscribe to the channel for more rapid codecasts on design patterns, refactoring and development approaches.

Jonathan Lee Martin

Jonathan is an educator, writer and international speaker. He guides developers — from career switchers to senior developers at Fortune 100 companies — through their journey into web development.