Async Control Flow without Exceptions nor Monads

Async Control Flow using Exceptions

The throw keyword is one of the most powerful features of the JavaScript language. It provides the one-and-only way to jump across multiple Call Stack Layers.

Throwing Exceptions is close to a “goto” in JavaScript—then there are control flow labels.

Remember, with great power comes great responsibility. You wouldn’t use goto in your code, so don’t use throw for Control Flow. Here’s a simplistic example.

The Exception Entanglement Problem

Overusing throw is a code smell because it’s likely that you will end up entangling Runtime Exceptions and Business Logic Errors. Merging them just pushes complexity downstream. Here’s what Exception Entanglement looks like.

Given two categories of things, would you rather have the ability to know the difference between them or not?

Merging two categories is easy — if that’s what you want. Splitting them back up is hard. Aim for simple, not easy.

Synchronous code is easy to fix

Just don’t use throw for Control Flow — use return only.

Callbacks don’t need fixing

Callback-style programming has two separate error abstractions.

  • Business Logic Errors are reported with the first argument of the callback-function — cb(err, null).

The problem lies with Promises

In Promise-land there’s only one abstraction for errors: Rejection.

The Promise abstraction robs you of the ability to handle different things differently.

Today Promises are universal. The JavaScript ecosystem was thirsty for a solution to Callback Hell and the Pyramid of Doom, and it drove developers to eagerly jump on the Promised bandwagon.

Solution #1: Fixing (broken) Promises

I’ve thought about this problem for a long time, but couldn’t find much on the topic. Recently I came up with the perfect search query: “javascript async await without exception for control flow”, which took me to How to write async await without try-catch blocks in Javascript (d’oh).

Returning an error-first pair

Grossman proposes to convert Promises into an error-first pair (much like callbacks do). He’s written a library called await-to-js.

Alternative #1: Ditch Rejections and always return [err, value]

One way is actually not to use await-to-js. Simply have async functions return the [err, value]pair. That means using the first argument of [err, value] for Business Logic Errors and leaving Promise Rejections solely for Runtime Exceptions.

Pseudo-code.

Alternative #2: Filter native Error classes

How can we solve the remaining second half of the problem, while still being able to consume libraries from npm?

Solution #2: The Functional Approach

The Promise abstraction provides both an easy API to defer a computation until a later time and the ability to create computational chains — nothing that can’t be done in user space with a library.

So don’t we “futurify” Promise-based packages on-the-fly?

Because you can’t await “proper” Monads — just Promises.

Giving up async/await is a scary thought.

What is `await` anyway?

When you think of it, the await keyword is nothing more than a Promise-specific do-notation. It makes sense it has been implemented this way, since ECMAScript was already committed to the Promise abstraction.

If ECMAScript had implemented “proper” Monads, we could have “proper” do-notation by now.

Note that I am not saying it would have been better — just saying it would have been different. But that didn’t happen. No point in spending energy trying to curb reality into idealistic scenarios — that’d be insane.

Async/await is just syntactic sugar for calling .then()

It seems the way JavaScript knows whether something is a “thenable” or not is through duck-typing inferring. Which means you can await anything with a then method. Let me repeat that:

You can `await` anything with a then() method.

(You can await anything really. For example, await 'foo' wraps 'foo' in a Promise and immediately resolves it, but that is less useful to us.)

Let us implement do-notation with generators!

Generators are the foundation of async/await, so there should be a way to use them to implement “proper” do-notation.

Interop with Promise-returning libraries from npm

Monads don’t natively coerce Exceptions into Rejections — so no Exception Entanglement problem here. But what would happen when you consume Promise-returning packages from npm?

(Non)-solution #3: Quit JavaScript

Researching for the present article gave me an even greater insight into what JavaScript’s strong and weak features are. We can take advantage of JS’s extensive compatibility without having to write actual JavaScript code.

Afterword

I can’t begin to tell you how excited I am! Giving up on async/await was never an option for me, but now that I’ve found do-notation for JS it is not a problem anymore.

5.649m — Cerro Toco, Chile.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store