Breaking free from the request and argument drilling with AsyncHooks

Published 4/6/2020

I didn't pay much attention to the features that landed in Node 13, something I will definitely do from now as they have introduced an absolutely amazing new feature.

From frameworks in other languages like Laravel you might be used to getting the authenticated user like this:

use Illuminate\Support\Facades\Auth;

// Get the currently authenticated user...
$user = Auth::user();

You can only get the authenticated user through the request as this is where you have access to cookies, headers, etc. Yet, the above code example doesn't mention anything about the request. Laravel provides us this expressive facade and does the dirty work under the hood, where it still accesses the request.

This is possible in PHP because each request is completely isolated from one another.

In Node.js, for a long time, you needed to get the authenticated user from the request and then pass down to every function that needs it.

class PostController {
  async index({ auth }) {
    await new PostService().fetch(auth.user)
  }
}

Until now.

Node 13 comes with a new feature called AsyncLocalStorage.

From the documentation it says: It allows storing data throughout the lifetime of a web request or any other asynchronous duration.

And here is an example:

const { AsyncLocalStorage } = require('async_hooks');
const http = require('http');

const requestKey = 'CURRENT_REQUEST';
const asyncLocalStorage = new AsyncLocalStorage();

function log(...args) {
  const store = asyncLocalStorage.getStore();
  // Make sure the store exists and it contains a request.
  if (store && store.has(requestKey)) {
    const req = store.get(requestKey);
    // Prints `GET /items ERR could not do something
    console.log(req.method, req.url, ...args);
  } else {
    console.log(...args);
  }
}

http.createServer((request, response) => {
  asyncLocalStorage.run(new Map(), () => {
    const store = asyncLocalStorage.getStore();
    store.set(requestKey, request);

    setTimeout(() => {
        log('timeout');
    }, 2000);
  });
})
.listen(8080);

As you can see, when we wrap all our code within asyncLocalStorage.run, anywhere within the callback we can again retrieve any data we stored away. It's just like react's context API!

Building a clean abstraction layer around this is not too hard.

Check out my e-book!

Learn to simplify day-to-day code and the balance between over- and under-engineering.

I'm excited about the possibilities this opens up. Here are some use cases I can think of:

  • Getting and setting cookies
  • Logging information about the current request
  • session flashing
  • Wrapping database calls in a transaction without passing the transaction object to each query (sometimes in separate function/class).

This was just meant as a short introduction to this new feature. Obviously, this also opens up room for complexity and dirty code. For example, it lets you access the request payload from anywhere in your application. Something you probably don't want to make use of in too many places as it couples the request with your entire code base. Well, let's see where this takes us!