Sync vs. Async: The Odd Couple of Code, Can the `Result` Keep the Peace?

jasuperior

Ja

Posted on October 16, 2024

Sync vs. Async: The Odd Couple of Code, Can the `Result` Keep the Peace?

Bringing Rust's Result type to Typescript and giving it a bit of zhuzh


Admittedly, I created this implementation of the Result type before diving into constructing a new enum type in TypeScript. Some of it was for fun, but I’m also exploring implementation options for my current project. That said, there’s already a new version of this type, building on concepts I discussed in my previous article on Rust Enums in TypeScript. I'll write a follow-up article once that's complete.

It's 9:00 AM, and you've just opened your laptop. After scrolling past emails about pizza parties and the latest “AI revolution,” you realize your task today is to handle errors—without turning your code into spaghetti.

You’ve been here before. The last time, you tried to ignore error handling, thinking, "Surely nothing will go wrong." Then, like clockwork, you’re wading through a minefield of undefined values and awkward try/catch blocks that feel like bubble wrap for your code: a whole lot of work for little payoff.

And here’s the kicker: today’s function is async. Great! Now everything’s a Promise! Suddenly, your function stack is more tangled than your headphones at the bottom of your bag, and you're juggling awaits like they’re going out of style. But there’s hope: you remember something from the distant land of Rust—a land where Result types keep error handling sane.

So, the journey begins. What if you could bring a little of that Result magic to TypeScript? What if, instead of tap dancing around exceptions, you could handle success and failure with the grace of a well-oiled IDE?

What’s a Result Type? (And Can It Fix My Life?)

Let’s be real: if JavaScript is the Wild West, then TypeScript is the sheriff, walking around town enforcing law and order with types. But sometimes, even TypeScript’s badge isn't shiny enough to save us from errors lurking in the shadows of undefined.

Rust, being the responsible adult of programming languages, uses a Result type to make error handling explicit. It’s simple: every function either returns Ok(value) for success or NotOk(error) for failure. Nothing gets swept under the rug.

In TypeScript, however, the best we usually get is null, undefined, or (heaven forbid) a surprise throw statement. It's like getting ghosted by your code—one minute it's there, the next... poof.

But what if TypeScript had a Result type? You could:

  1. Clearly indicate whether something succeeded or failed, without any surprise parties.
  2. Chain together a series of operations that may or may not fail, while maintaining the flow of your program.

Which brings us to our hero: the TypeScript Result type, a Rust-inspired solution to all your error-handling woes.

Function Coloring: Why Your Code Looks Like a Rainbow Gone Wrong

Image description

You’ve probably run into the problem of function coloring without even realizing it. It’s that frustrating moment when you write a perfectly simple, synchronous function, but then you throw in a promise... and suddenly everything downstream needs to be async. It’s like adding garlic to a recipe—now everything tastes like garlic.

Function coloring happens when you mix sync and async operations in a way that makes your code look like a Jackson Pollock painting (except, you know, with await splattered everywhere). It’s ugly. And debugging it? It’s like playing chess with a blindfold on.

But our friendly Result type solves this issue by wrapping both sync and async values with the same API. So, no matter what you throw into it, you can handle it in a unified way—whether it’s a plain old number or a promise that resolves after your lunch break.

Introducing the Result Type: Now in TypeScript Flavor

You might be thinking, "This all sounds great, but how does it work in TypeScript?" Don’t worry, we’ve got you covered. Here's a quick rundown of how the Result type operates in our universe:

  • Ok(value): The success case. Everything is fine, no alarms, no surprises. Just like finding out your npm install actually worked the first time.
  • NotOk(error): Something went wrong. Your program is still running, but it’s like that one friend who shows up to the party and immediately spills a drink.
  • Pending(promise): A result that hasn't resolved yet. This one’s still brewing, like the coffee that’s been "pending" in the office break room for the last hour.
let success = Result.Ok(42); 
let failure = Result.NotOk(new Error("404: Code Not Found"));
let pending = Result.Pending(fetch('/api/some-data'));
Enter fullscreen mode Exit fullscreen mode

You’ll notice a beautiful thing here: whether it’s a number, an error, or a promise, you handle everything in the same way. Gone are the days of try/catch nests and .then() pyramids.

Composability: The Real MVP

It's 2:00 PM, and you're in the thick of it. You’ve got this complex function with multiple steps: get some data, transform it, maybe do some async processing, and spit out a final result. You’re about to face a nightmare of chained .then()s, but you remember—you’ve got Result.

Here’s the beauty of Result: you can compose functions like a LEGO set, snapping them together with confidence that each piece will either fit or fail gracefully.

let process = compose([
    (x: number) => x * 2, // Multiply the input by 2
    (x: number) => `${x}px`, // Add some CSS flavor
    async (x: string) => x.split("").reverse().join(""), // Flip it around, async-style
    (x: string) => Result.Ok({ result: x }) // Wrap it up in an Ok, like a cozy blanket
]);

let result = process(21);
Enter fullscreen mode Exit fullscreen mode

Each function step is passed through the Result wrapper. Even if one step blows up (like that weird async thing you wrote at 3 AM), the whole thing doesn’t collapse. The Result just passes the failure down the line, neatly contained.

Now, let’s imagine this in a normal day:

  1. You call the composed function.
  2. Each function gets the output of the last, wrapped safely in a Result.
  3. If everything succeeds, you win! If something fails, you get a polite "NotOk" that lets you handle it later, instead of dealing with a screaming throw.

And if part of your function needs to wait for some async process? No big deal. Result will wait patiently like a well-behaved promise. No extra awaits. No .then() chains that look like modern art. Just clean, readable, color-free code.

Sync vs. Async: The Great Unification

The best part? You no longer have to deal with TypeScript’s split personality between sync and async functions.

With Result, whether your function returns a regular value or a promise, it’s all wrapped in the same Result type. So, no more juggling. Just one clean API to rule them all.

const result = Result.Ok(42)
    .map(x => x + 1)
    .map(x => `${x}px`)
    .map(async (x) => await someAsyncFunc(x)); // Whoa, async? Still works!

console.log(await result.getOr("default value"));
Enter fullscreen mode Exit fullscreen mode

There it is—the Holy Grail of modern TypeScript development: sync and async playing nicely together in one tidy chain of Results.

Conclusion: Saving Your Sanity, One Result at a Time

So, what have we learned today? By adopting a Result type in your TypeScript codebase, you can:

  1. Say goodbye to function coloring problems, and handle async and sync code in a unified way.
  2. Create beautiful, readable code that actually handles errors without clogging your codebase with endless try/catch blocks.
  3. Chain operations together, safely passing values from one step to the next, even if something blows up.

Now, your code can thrive in a world where every function politely returns a result, even when things don’t go as planned.

And you? Well, you can finally close that ticket on error handling without sweating the small stuff—because Result has your back. Now go ahead, grab another cup of coffee, and take a break. You've earned it.

One Last Thing...

I'm aware that there are dozens of other fairly good ADT / Effects library.... like Effect for instance, which has much of the functionality that I like. I just think its far too bloated for my use case, and is far to verbose to setup your types. I'd like for the type system to assist me forward rather than the other way around.

Also, I may publish this officially on my Github. I was nearly done all my tests, until I fell upon a requirement that I just simply could not ignore, so i've kinda gone back to the drawing board. However, if you would like to play with it, or fork it for yourself, I posted a Gist with the code on Github.

NOTE: this implementation requires TS >= 5.0

PS : This article probably requires some edits, but I'm tired... I've been up all night, and i'm going to bed now... NIGHT NIGHT! I'll expand on a few things later perhaps when I'm better fueled.

💖 💪 🙅 🚩
jasuperior
Ja

Posted on October 16, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related