I Promise this was a bad idea

Promises are being added to everything. Browser API’s, the latest cool frameworks and libraries, browsers, and soon, maybe even node core.

In theory, I’m all for it. In theory, if we had a clean, simple way to describe asynchronous interactions, it’d be a great idea. In reality, Promises fall short of the mark, and their relationship with async-await is one for concern.

In theory…

The best parts of JavaScript have been the result of goat tracks, a path beaten by thorough usage. Promises started getting used in some form of anger in libraries like jQuery, and in the cases for which they were being used, they pretty much did everything developers needed. Want to load a file and then parse it? Easy. Want to load a file, then load a few files based on the first? No problem. Throw an error? Cool, just show an alert(). For trivial use cases, Promises look great.

But there are downsides that make promises not very pleasant to use.

Bother.

They catch thrown errors and handle them the same way as rejections. This is a massive pain if you want your server to crash when you’ve made a coding mistake, as you have to try and detect ‘good’ errors, like 404s to external services, or DB lookup misses, from ‘bad’ errors like “TypeError: 555t5tt55555555 is not a function” – something that occurs frequently when you own cats. There is discussion around modifying how try-catch works in JavaScript to work around this issue, but that should be a massive red-flag in its own right.

Promises also don’t interop with existing API’s nicely, you have to write boilerplate to interact with the normal ‘error, result’ pattern.

Promises run eagerly. By its self this isn’t an issue, there are cases where this is good, but often, lazily requesting asynchronous dependencies is preferable. Creating a lazy dependency graph also allows for easier debugging and description.

What then?

There is nothing special about Promises, they were just big first. There are many alternatives that do a better job. righto and pull-streams do everything Promises do, with fewer pitfalls. Righto interacts with err-backs, and promises, with no boilerplate, won’t catch thrown errors, and can even print a trace that shows the dependency graph for a flow, and which exact task rejected.

debug

But Mah Async-Await!

The biggest issue with not using Promises that get’s raised is that they are functionally required for async-await, but they aren’t. There is nothing specific about promises that makes them necessary for async-await to work; any async task wrapper could be used just as well. Async-await supporting Promises exclusively is a conflation of concerns.

If async-await was more generic, it would be possible to use it with a wide array of third party libraries that expose the right API. pull-streams, righto my-cool-lib could ALL be used with async-await.

GOAT TRACKS

This restriction is a major concern for me, as it lays down an iron path that restricts the formation of goat tracks. It enforces the usage of Promises, even though there may be better options out there. It restricts experimentation with alternatives.

I’m personally not a fan of the async-await pattern, as it attempts to hide asynchronicity with synchronous looking code, deterring understanding. If it is going to be used widely, it should be at least given the opportunity to be used in a variety of ways, to see what works best.

I’m anxious of the day we all look back and think “Oh no, that was a huge mistake.”

– Contains opinions.

Advertisement

2 thoughts on “I Promise this was a bad idea

  1. Sam Danielson

    Nice writeup, but I disagree. The then chain executes strictly but the (reject, resolve) exposed to the executor do not. Those will be be event driven. The ES6 promise is quite general and the interface is well-considered. You have access to three things. 1) new Promise, 2) resolve, 3) reject. That’s really it. But then, what is the interface to an “if” statement, or to throw/catch/finally? These are simple yet composable things that can lead to surprisingly complex behavior.

    Because async/await is backed by promises we have 1) alternative syntax, 2) rigorous semantics in the promise/a+ spec. async/await actually decreases expressive power compared with functional composition because Promise.all and kin have no support. This decreased-power => easier-to-reason implication is similar (IMHO) to moving down a level on the Chomsky hierarchy. Like the ES6 class syntax, the proposed async/await syntax is a limitation on currently existing expressive power to improve execution in the human brain.

    I really like the atomic-by-default evented concurrency model. Compared to another great model, Actors, evented concurrency is the more machine centered approach. One man doing many things vs. many men doing one thing. Events are the former and actors are the latter. Events and actors are complimentary in this regard. So we have two complimentary models. For events, promises are the best abstraction we have. Speaking of calculus, promises are a monad. ES6 native promises are a bit clunky to extend due to their minimal API, but that’s a feature. 99% of the time you don’t want to extend them. When you do, you just have to follow the rules. Aaaaand… one last benefit of async/await being an extension of promises… Because I can extend promises, I can extend async/await.

    Thanks for the discussion. Promises are a very interesting topic.

    Reply
    1. Kory Nunn Post author

      So this is a description of the things you like about Promises, but doesn’t seem to address any of the problems I listed.

      For starters, the only requirement for interaction with async/await, technically, is a callback, did the task succeed or fail. Promises do a lot more than that, and that is not a good thing.

      By bundling async/await with Promises, there is absolutely no way to use async-await without catching all errors, and no way to distinguish between thrown errors, and rejected tasks.

      This isn’t Promises VS nothing, this is Promises VS many other better solutions.

      To prove that there is nothing amazing about async/await, check out https://github.com/KoryNunn/righto#generators-yield. The only difference between yielding generators, and async/await, is that eventually, async/await will be able to continue while the critical path has no requirement on unresolved Promises. There is absolutely no technical reason why Promises are the only thing that can do this, and support for a much more generic, low-level, non-catching API could be added trivially.

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s