Functional Pipelines in JavaScript Using async/await

Published: 2020-09-19

Did you know that async/await syntax is great for synchronous operations too? Really.

Let's say you want to perform some operations on a string:

let input = " some user input <script>alert('pwnd!');</script>";
input = input.trim();
input = sanitizeHTML(input);
input = `You submitted <pre>${input}</pre>`; 
display(input);

This example is a bit contrived, but bear with me. The point here is that there are a number of self-contained operations we want to perform on our input string input.

How could we improve this?

There's nothing wrong with the above example, however, reassigning a variable just doesn't feel quite right... what we have here is a functional pipeline, so it would be ideal to be able to model our code as a functional pipeline as well.

Manual Functional Composition

One way we could change this code is to add a pipe function that will let us actually create a functional pipeline.

const pipe = (...fns) => fns.reduce((f, g) => (x) => g(f(x)));

// Build up a functional pipeline
const fn = pipe(
  x => x.trim(),
	x => sanitizeHTML(x),
	x => `You submitted <pre>${input}</pre>`,
  x => display(x),
);

let input = " some user input <script>alert('pwnd!');</script>";
fn(input); // Run it

This is a valid approach and one I've used in the past many times. However, it has some drawbacks:

  1. This type of code is often opaque to new programmers or programmers without any background in functional programming.
  1. For synchronous operations it requires building up a function even if you really just want to run a series of functions immediately and use the result.
  1. For async operations we can still use the approach above but may become harder to reason about and it will almost certainly exacerbate the first caveat, becoming less readable to outsiders.

Promises can help

Now let's rewrite this functionality using promises.

const main = async (input) => {
  const x = await Promise.resolve(input)
		.then(x => x.trim())
    .then(x => sanitizeHTML(x))
		.then(x => `You submitted <pre>${input}</pre>`)
	
  display(x);
};

main().then(() => console.log('Complete.'));

Look at that, all our operations neatly pipeline without any external libraries. What's more, we can now handle async operations and sync operations together in the same way.

Let's assume display is an async operations. In the above example we could simply add an await in front of it and our function would continue working as normally. Or we could include it in the pipeline:

Promise.resolve(input)
		.then(x => x.trim())
    .then(x => sanitizeHTML(x))
		.then(x => `You submitted <pre>${input}</pre>`)
    .then(x => display(x)) // Even if display is async this will work 

The point

Promises are pretty flexible, and can be used to model both sync and async operations without any external library code.


About me

I write about life as well as my mistakes and successes as I learn to build a business. I'm building a self-funded startup (Pairwise).


PrevAll Posts

Thoughts? Let's chat on Twitter or via Email :)