'Do' More With 'Run'

Max Greenwald | 19 May 2023

I recently wrote about asyncPool, one of my favorite JavaScript / TypeScript helpers, and today I want to share an even simpler yet extremely useful utility: run!

Here's the definition:

function run<T>(fn: () => T): T {
return fn();
}

Or, a one-liner:

const run = <T>(fn: () => T): T => fn(); // This is the whole thing, I promise!

Now, you're probably thinking to yourself: "That's a silly function! It just calls a function." And yes, run is simple, but its deceptively useful too.

run has two primary use cases:

  1. IIFE (Immediately Invoked Function Expression)
  2. do expression (currently Stage 1 Proposal)

Lets look at some examples.

1. Use as an IIFE

When acting like an IIFE, run takes a sync or async function and...runs it! To my eyes, the version with run is cleaner.

// BEFORE: Normal IIFE
(async () => {
console.log("Lots of parens");
})();
// AFTER: With run
run(async () => {
console.log("That's better. Great for bin scripts!");
});

2. Use as a do expression

do expressions are a Stage 1 proposal for JavaScript that are oriented towards functional programming and composability. Here's an example:

// This is a proposed syntax for do expressions.
// Do not use this in production code!
let x = do {
let tmp = f();
tmp * tmp + 1
};

Unfortunately, this proposal is a few years old and seems stale. Fortunately, we can get almost all of the benefit of do expressions with run.

When acting like a do expression, run executes a function and returns its value, which brings with it all the benefits of early returns via if/else statements without needing to extract the function out of its parent function context.

This is handy for a few reasons:

  • Inline early returns
  • Simple variable assignment
  • Receive parent scope
  • Don't need to declare and name a function
// BEFORE: let declaration and manual assignment
function doWork() {
let x;
if (foo()) x = f();
else if (bar()) x = g();
else x = h()
return x * 10;
}
// AFTER: using run to simplify variable assignment
function doWork() {
const x = run(() => {
if (foo()) return f();
else if (bar()) return g();
else return h();
});
return x * 10;
}

Use in Templating Languages

These do-like expressions are especially helpful in templating languages like JSX where there is conditional logic requiring if statements.

And, let me be clear: multi-line ternary operators are bad. Nobody wants ternary hell. Long ternaries are unreadable and hard to refactor, and I see them all the time in React projects.

Wouldn't this be nice to use in your next frontend project?

return (
<nav>
<Home />
{run(() => {
if (account.status === "loading") return <LoadingPage />
if (account.status === "signed-out") return <SignUpPage />
if (account.status === "onboarding") return <OnboardingPage />
// If "account" is a discriminated union, TypeScript can infer "user" here!
const user = account.user;
return <DashboardPage user={user} />
})}
</nav>
)

And now you might be thinking "but why not just make that another component?". And sure, you could do that, and in some cases it makes absolute sense to do that.

But, in many cases, your complex conditional rendering does not need to be abstracted into new components. Instead, you can break the pieces of rendering into logical run functions. This removes the need to make largely useless display components just to benefit from early returns.

Advanced Use Case: Promises

When working with Promises, run makes more sense than do because it doesn't use the async scope of the parent and instead allows you to create and execute async functions inline.

Let's say we are writing a deploy script, and we are deploying two things at once: 1) a dockerized server and 2) lambda functions. In this case, run can be used to build and deploy both at the same time while maintaining parent scope.

Remember: run returns the result of running the function it is passed. When passed an async function, it will return a Promise.

import "zx/globals"; // A great library for bin scripts
const projectId = process.env.PROJECT_ID;
const version = process.env.VERSION;
// Main function
run(async () => {
// Kick off functions deploy
const functionsPromise = run(async () => {
await $`pnpm --filter functions build`;
await $`cloud-of-choice deploy functions --project ${projectId} --version ${version}`;
});
// Kick off server deploy
const serverPromise = run(async () => {
await $`PROJECT_ID=${projectId} VERSION=${version} pnpm --filter server build`;
await $`docker push registry.hub.docker.com/great-project/server:${version}$`
await $`cloud-of-choice deploy server --image registry.hub.docker.com/great-project/server:${version}`;
});
// Wait for both deploys to complete
await Promise.all([
functionsPromise,
serverPromise
]);
})

Conclusion

In my most recent project, I used run over 300 times in a variety of circumstances. I used it everywhere: bin scripts, backend code, and frontend code. I love how run makes my code more readable and functional. And, if you use TypeScript, run is the best for ripping apart discriminated unions and getting the correct type inference. Try it once and you'll be hooked! (run is also useful in hooks like useMemo but that was just a figure of speech)

I hope that this simple utility will catch on so we can stop using nested ternaries and simplify complex async operations. If you agree, spread the word by sharing this post!

FAQ

Q: "Max, will you make this into an npm package? That would be really handy!"
A: No, its one line. That wouldn't make any sense. Ready, copy, paste!