Promises are not neutral enough
staltz.com
staltz.com
1. Eager, not lazy - I think it was a mistake for the promise constructor to take a function, and in that way lead the users to believe the promise represents a computation. Creating a pair of promise and future (the latter as the producer side, like in C++) would be much cleaner. I disagree that lazy would be more general, you can simulate lazyness with functions, but you couldn't eliminate the performance cost of creating the unnecessary closure with a lazy solution. Regarding getUserAge - the common case for that function would be to take the user ID as the parameter (and hence would be lazy by construction), the parameterless version is a special case.
2. No cancellation - cancellation is much better represented with cancellation tokens (even C# Tasks cancel with cancellation tokens, so does fun-task mentioned at the end, though in non-composable way) - you cannot build a generic solution that can cancel the right computations. With cancellation tokens it's clear what cancels what.
3. and 4. (as well as being allowed pass non-promises to places where only promises make sense, like Promise.all and await) are unfortunate accidents that make typed environments (e.g. TypeScript) harder to work with but are not that important as 1 and 2.
Constructor doesn't just take a function, it takes a closure if you so choose.. A closure that has immediate access to resolve and reject and doesn't have to worry about other code paths access its enclosed vars first.
Promises are a great example with the problems of believing that something good in one language will be good in another. Promises in JavaScript are fighting the language, because JavaScript is fundamentally a collection of isolated but contextual behaviors.
Using Agents to encapsulate callbacks is easier to reason about, easier to test, less likely to swallow errors whole, and doesn't have any of the problems laid out in this article. Unfortunately because it isn't a model popular in any other language, it doesn't have the name recognition Promises do.
What was your precise experience?
In practice, this behavior causes very real problems and in node land they made the sane decision to kill the process whenever an unhandled promise rejection comes about. I don’t know if this has landed yet, but you’ll see a warning about it if you run a node process in which you reject a promise.
Throwing on unhandledRejection has been the default in Node.js >= 7 and browsers now have DOM Levels 1 and 3 events for unhandledrejection.
Regardless, the original poster was correct – this has been a sin of promises for a long time. What’s worse I think is that because of this we move have semantics that are close but not quite the same as exceptions. Case in point: throwing an exception while executing a promise function will reject the promise. But it’s not an exception anymore, even though the value of the rejection is in fact the exception. The semantics are now different – because promises.
Promises in JavaScript have a certain almost-but-not-quite quality to them.
Sure there is. What else does the `throw` statement do, if not throw an exception?
[1] http://www.ecma-international.org/ecma-262/6.0/#sec-try-stat...
[1] http://www.ecma-international.org/ecma-262/6.0/#sec-try-stat...
In fact, if promises worked the way he wanted them to, it would hurt the ecosystem in every category he mentions. Lazy promises would cease representing a single value, and be un-cacheable. Promises that didn't flatten inner promises would create endless confusion and ambiguity over "onion-promise" scenarios. Sometimes-synchronous promises would introduce subtle and sometimes catastrophic runtime ambiguities (aka "release zalgo"). Even cancelable promises would raise thorny issues regarding whether promises are intended to be multicast or unicast, which is a problem the current design side-steps entirely.
Here is an example of how promises limit the power of mobx
https://twitter.com/spion/status/958906847385341952
Another example relevant in node is continuation-local-storage (equivalent to threadlocal storage). Implementing it on top of generators or other "chainable / thenable" abstractions is trivially easy. Implementing it on top of native promises and async/await is impossible without deep hooks into the platform.
More examples here: https://spion.github.io/posts/es7-async-await-step-in-the-wr... (see the second part)
We should've paused on async-await and waited for jhusain's compositional functions: https://github.com/jhusain/compositional-functions
In the meantime generator based libraries would've properly explored the whole breadth of power that co-routines can give you, creating cowpaths to be paved by TC39.
Promises make trade-offs, and they end up with a design that is generally good and can be used well in some number of situations. But not all. Not nearly enough to get first class syntax support that makes them privileged over all other solutions.
What if you could simply `yield getCurrentUserSession` and the engine which ran the toplevel generator returned it back to you?
jhusein's compositional functions solved the syntax issue.
And yes, promises limit it.
The "market" of application state management libraries.
"Slightly confusing" == I'm not familiar with it. Not really an objection.
"No structural comparison": don't know what this is supposed to mean.
Re: laziness, actually redundant recomputations are not performed. Not sure where you go that impression.
Slightly confusing is a real objection. MobX strives to implement transparent reactive programming, where the way you access, update and transform values works exactly like it would with regular objects. S.js has a worse learning curve.
Redundant computations are computed; see: https://codepen.io/anon/pen/vdWomE?editors=0010
even though the completed() computed isn't used anywhere, the reduction is still being recomputed every time todo state changes
Hardly: reactive values are get/set functions, and you create new ones with S(() => ...). That's it.
MobX's attempt at transparency yields inescapable and surprising corner cases.
> Redundant computations are computed; [...] even though the completed() computed isn't used anywhere, the reduction is still being recomputed every time todo state changes
Incorrect. The todos binding is an SArray not a regular array [1]. See my modified version where I log the events to the console [2].
I'm willing to accept a few corner cases as long as there is a large common subset of functionality that works both with and without a small number of decorators. This can be utilised to write models that can be used in both a reactive and a non-reactive context with a different set of decorators injected in. S.js looks too invasive to do this.
Your codepen has no completed todo count. Why are the recomputations logged every time here?
https://codepen.io/anon/pen/vdWomE?editors=0010
> That's it.
What about the poorly named "S.freeze"
Because you can't apply reduce any other way. It's a computation defined over a whole collection. To incrementalize it, you'd need to be able to invert whatever function you're trying to apply in order to arbitrarily undo and redo the operation as elements are added/removed. This is literally impossible in general as not all functions have inverses.
You should also use the SArray methods directly rather than embedding within S(): https://codepen.io/anon/pen/NyXqjB
Edit: you can see a semi-incremental version exploiting map's semantics here: https://codepen.io/anon/pen/KQZddK
If you look at the docs for SArray, you see they describe that map avoids recomputation.
> What about the poorly named "S.freeze"
What would you call it? S.atomic? I don't see how the existing name is particularly unsuitable.
In mobx, an unused computed will not run or recompute until its requested by a side effect (an autorun reaction, an observer component etc).
Example:
https://jsfiddle.net/ycufg9d3/
We use this to great extent in our application, by only rendering components that are visible in the viewport at the moment.
You can also keep its cached value alive, but not recompute it until needed by implementing a reaction that observes a computed but doesn't request its value. This will keep the entire computation graph cached but idle and partially dirty until the value is requested, at which point only stale dependencies will be recomputed. This can be extremely powerful: for example you can implement a state tree undo/redo by implementing serialize, then observing the serialize computed for the root item without requesting its value (keeping things cached) and only requesting recomputations when certain sufficient number of mutations are made. (with the vast number of reused values being structurally shared between undo/redo states)
> What would you call it? S.atomic? I don't see how the existing name is particularly unsuitable.
Yes atomic would be an improvement. Freeze only makes sense if you are thinking in terms of FRP signals in time, and its unclear whether the abstraction tries to hide its signal underpinnings or expose them (its somewhere inbetween)
MobX will stop dependant recomputation if the new value of an intermediate recomputation is equal to the previous value.
You also have the option of using structural equality instead of reference equality (and also you can use any custom equality comparison function)
https://mobx.js.org/refguide/computed-decorator.html#options...
On opinionated choices: I try to base that on mathematics. When it comes to async programming, that means equational reasoning and following some basic properties such as composition, associativity, left identity, right identity, etc, see https://github.com/fantasyland/fantasy-land . These would be neutral because math is often neutral. For instance, if intelligent aliens exist, they probably figured out the circle just like we did.
On ideological preferences: there isn't anything free of ideology, so yes I am motivated by some ideology, not unlike influential members of TC39. There, you'll often find opposition to functional programming ideas, even though JS was originally designed with influences from Scheme, and allowed functional programming better than, e.g., Java at the time. See these TC39 notes, for instance: https://github.com/tc39/tc39-notes/blob/master/es8/2017-09/s...
FP ideas are usually opposed because TC39 proposals are driven by concrete use cases, and FP is all about abstractions. FP ideology is that abstractions are good because they accomplish abstract goals, not a single particular concrete goal. That does not play well with the use-case-first process at TC39. So, ideologies.
On "it would hurt the ecosystem": I think by now a lot of people from different language communities recognize the success of RxJava/RxJS/Rx.NET, and it tackles all those complications you mentioned, but for even more complex use cases, because it handles multiple values over time. I'm not arguing that Rx is a silver bullet, I'm just saying the Rx community has first hand experience with solving and teaching solutions to the problems you mentioned in the second paragraph and it's nowhere near "catastrophic".
Promises are meant to semantically (and with async/await, syntactically) resemble a function call as closely as possible, while minimizing confusing errors. The author's "improvements" would ruin this.
If I need a bunch of features that a promise doesn't provide, like cancellation, I will write something to do it manually with callbacks. This is far less than 1% of async calls though.
Or you could consider the possibility that they aren't arbitrary at all. Perhaps these properties are well motivated by, for instance equational reasoning, which is a cornerstone of extensible and maintainable programming.
> Promises are meant to semantically (and with async/await, syntactically) resemble a function call as closely as possible, while minimizing confusing errors
Why? What purpose does that ultimately serve given we already have functions? The whole point of an abstraction is to provide sophisticated semantics to do an important job you would otherwise have to do by hand.
What are the other cornerstones?
If I write
function foo(v) {
return function () { return v }
}
const fv = foo(4)
then I expect fv will be a function, not a number. In that respect, I think the choice to make `then` flatten was a poor decision that makes it more confusing for people who don't really take the time to understand. Such people don't realise it's confusing: they simply notice it's usually convenient; but they're inhibited from drawing the correct analogies.But as for the rest. Yeah, it's just arbitrary and weird. Maybe we should make integers all arrays lazy by default too?
In this case, not being able to synchronously resolve a promise prevents you from using them in certain ways. You can attach flatMap to the prototype or build a better way of doing cancellations, but you'll need to make additions to the language itself to turn a promise synchronous, which is what await/async were all about.
Having said all that, the bar for promises isn't set by RxJS, it's set by callback(err).
EDIT: also, promises were very much discovered. They're an opinionated solution to the very specific problems of callbacks. They're chainable because of callback hell. Errors propagate because that's a problem with callbacks. Their execution is always delayed because callbacks made it hard to reason about execution order. They don't handle cancellation because callbacks don't handle cancellation.
You order a burger at the cashier window, then go to the pickup window. If the burger is already made, it's already at the pickup window when you get there.
The author wants a special case where if the burger is already made, they hand it to you immediately at the cashier window. This might seem more efficient, but both in the restaurant and in code it makes logic way more complex.
I can see the automatic unwrapping of Promises to be an issue in some libraries that want to make specific guarantees, but in most of my code this has behaviour has simplified things.
I definitely prefer having Promises and async-await right now over another theoretically sound (but probably more verbose) system available in a couple of years.
I however write a lot of systems where almost all operations need to be concurrent, cancelable and rate-limited.
Personally I find callbacks and the event loop easy to reason about, but too daunting when all you do is CRUD requests to a database.
The problem with Promises though is that they spread, they don't like to live side by side with other async paradigms.
In my (more or less extensive) experience missing cancellation is what bites you more. Not because Promises don‘t implement it (I worked a lot with bluebirds CancellablePromises a lot and… it’s not fun) but because there’s no unified, “standard” and “specced” way.
AbortControllers (which are just abort tokens hidden as EventEmitters) are not that bad but we’ll have to wait a lot before all fetch implementation support it (I’m looking at you V8 and Safari!). I still don‘t understand why they didn’t called it CancelController since “abort” is such an overlodaded term.
Also (still in my experience) cancellation is incredibly more complex than it could seem. You don‘t want to simply stop the synchronous propagation, you want to act on it! You resources to be freed, you want partially completed async process to rollback what has already been done. Not easy.
Technically, the API documentation indeed says a Task has Start() methods i.e. is lazy.
But practically, in the majority of cases they are created already in Running or WaitingToRun state. This applies to tasks returned by asynchronous APIs in the framework, tasks implemented by user-written async methods, and tasks started with Task.Run() static methods. Calling Start() on them will throw an exception complaining about the wrong task state. So, in the current versions of .NET, the tasks are eager just like in JS.
I think the lazy tasks are mostly for backward-compatibility with older .NET framework 4.0 that already had tasks but didn’t support async-await.
For promise cancellation, this has been talked to death, but in short, making any function preemptable at any point in its execution makes writing correct code much much harder. As an example, I've got an API that takes independently cancellable requests. Multiple requests often need to calculate the same thing, so there's a cache. Any given promise in the system might be downstream of multiple requests. If cancellation is built into promises, how do I express how cancellation should propagate through the tree of promises?
A C#-style cancellation token API, orthogonal to promises, is simple, easy to build, and easy to understand.
The problem is that promises were added to the language without any of that being hashed out. Now you may say "Having language-level support for promises is useful; cancellation tokens are more library details", and you'd have a point. The flip-side, however, is that e.g. the Fetch API is only now getting any sort of cancellation ability, and currently I believe it's limited to Firefox and Edge.
Which is not to say that I'm even confident that language support for Promises should have been held up. But I understand the complaints.
I'm interested in learning more about this, do you happen to have any links to building such an api?
So you pass the token into a cancellable API, and that API calls token.throwIfRequested() in places where it is safe for it to do so (i.e. outside of critical sections).
A library implementing this in <100 lines of code (based on a spec that has since been sadly abandoned): https://www.npmjs.com/package/cancel-token
It allows a function to be cancelled from the outside, but only on its own terms, and gives the function a chance to clean up after itself.
I'm going to use this
So this looks like a trade-off: you could make function composition easier only by making Promises less suitable for their original purpose. By going generic, you lose an important guarantee that a Promise is just a value.
Also, cancellation is yet another state and it's hard to generalize especially when you don't have threads.
Promises should always be async because you'd want the result to be consistent. If I'm returning a promise and you are depending it to be sync, that means it weakens my flexibility. It makes the code harder to reason about.
There's a solution in delimited continuations, however delimited continuations seem to only be used and understood in the Scheme community (are they used anywhere else?). Delimited continuations allow you to suspend your code to a "prompt" lower in the stack at that point... and it doesn't matter if you have non-"async" code in between.
It'll be nice when they make their way to other more mainstream languages.
- async is relatively easy to turn into sync. The inverse isn’t true.
- no API design is ”neutral”. Any API design is opinionated. Cancelable lazy synchronous promises is just as opinionated a design as the current design.
The way you avoid unnecessary computation, when you have laziness, is to just roll it into the lazy semantics.
Have it so that if the promise generates something complicated, like a sequence, that the promise only generates as much of that something as is accessed (and maybe only a little bit beyond that).
In other words, the async promises should perhaps behave not so differently from synchronous lazy mechanisms.
The two are flipsides of the same coin. Say I have a synchronous lazy list (of strings). The strings come from reading a file. Ah, but reading a file is asynchronous at the OS level. So actually the list is asynchronous, in a sense. When we access the first element in the list, a line is read from the file. The underlying stream object reads an entire buffer-sized chunk, though: still synchronously. Moreover the OS behaves asynchronously and reads ahead in the file, caching more of it than the stream library asked for. Of course, the OS doesn't read the whole file (unless it's small). Just a little bit ahead. Enough ahead not to hammer the I/O subsystem with lots of small operations.
We can create this list over a log file that has 100 million lines, then read just the first 100 lines and stop using it. The underlying stream library might read 16K of the file, of which the 100 lines occupies only the first 8. The OS might have read ahead by quite a bit more than that and cached more of the file, and the hard drive's firmware might have buffered an entire track. If we don't read anything more from that list, then the operation is effectively canceled. The OS won't cache any more from the file; the stream library won't buffer more of text stream.
What about when you start composing Promises? For example, say I have a top-level promise that just returns a value. But under the hood, it needs a promise that generates an array. Even if the under-the-hood promise generates the array piece by piece, the caller of the larger promise is only even going to get a single value, so they lose the ability to "stop".
(You might imagine that the composed-over promises are network I/O, for example.)
How about this alternative: instead of .cancel() on promises have a .commit(). This is called if you're sure that you will eventually need that value. The calculation then proceeds full steam ahead: no going back.
You can ask for the value with or without .commit(); it is just a hint. But if you ask without .commit(), you may have to wait for a completion that was deliberately stalled due to your lack of commitment.
Without .commit(), async promises will still proceed on their own to some extent based on some fudge factor; we don't want programmers automatically calling .commit() on every promise they make to get the async benefit, which defeats the purpose.
Uncommitted promises could be identifiable to the garbage collector and subject to an internal cancelation protocol between GC and the promises. That protocol basically helps the promise's thread vacate the object so it can be reclaimed.
Promises could have some sort of hint about how far to proceed before requiring commitment. This would have to be well thought out: such hints tend to be too system and workload specific. Automatic tuning is better. The promise system could keep some statistics about how soon various kinds of promises are called in after being initiated, and how often they are called in at all, and then uncommitted promises could decide based on that how far to compute.
I do think `p = new Promise(fn);` shouldn't kick off the `fn` immediately. But that it should start right away in the next event loop. I haven't had issues with creating promise getters for repeatable calls. And think it organizes the business code away from the low level code.
I don't see a problem with the original Promise.cancel() you proposed or how your lazy promises makes canceling them any easier.
And don't we have `await` for the synchronous problem?
console.log(await Promise.resolve('hello'));
console.log('world')
// outputs "hello" "world"It might just my own mental model. But if I'm using a promise, it is a future value. So I don't understand why you would ever want to do that. Plus that's what Promise.resolve() is for. I'd expect them to act more like the now defunct setImmediate() function.
I concede that it may be more performant to do it this way as it results in less context switching, but I personally don't think performance should dictate a language's design of primitives.
Regardless, I legitimately believe the mental model of it not switching back and forth is fundamentally more correct for the primitive. The goal should also be that the abstraction has the same behavior and ordering semantics as doing it by hand. When you do it by hand, you essentially must run the code there immediately as otherwise there is no way to even run that code at all. Why would you ever want that code to be delayed? It frankly sounds like you are modeling Promise as if it meant Thread or something... a Promise is just a tiny adapter whose purpose is to change a wrap some setup code for accessing the value of a later event together into a common interface. A promise isn't doing the work: it is adapting the API of random models of doing future work (one-off callback APIs, random evented interfaces, etc.) to its own. Having this allows us to hack in (due to the lack of good monad support in most programming languages) async/await in a non-horrific way. If you want to do future work your first step should be to come up with a model for how your future work will happen, and then you use Promises just to do this adaptation, not to like, spawn some computation that will take time and which should happen later.
(async () => {
setImmediate(function() {
console.log("4");
});
setTimeout(function() {
console.log("3");
}, 0);
console.log(await new Promise((resolve, reject) => {
console.log("0");
resolve("2");
}));
})().catch();
console.log("1");This all happens synchronously, although deferred, but still can block the app completely if someone is simply wrapping synchronous actions in Promises expecting them to be "async".
async/await solves much of this, of course, but where that's not available I much prefer to keep all my async functionality actually async, and start off by using `Promise.resolve()`. Save the constructor for when you need to encapsulate some non-promise async code.
So why are promises the problem instead of the lack of libraries on top of them? I understand cancellation cannot be fixed, but laziness sure can. As for synchronous execution, that's just not gonna happen in event-driven-land. It doesn't with other callback-based APIs (except AJAX which is deprecated) and I don't see the complaints there.
I’ve been using redux-saga a lot recently and they really fill the gap between the concept of a long running Task and asynchronous values/executions (Promises).
There's still a lot that can be done with generators, so I don't see them falling out of favor completely yet.
There are about 8 million Task/IO monad implementations and no one stopped for a second to think that `task.fork` could just return a Thenable and work with async/await as expected.
Future.of(0).promise().then(console.log);
function LazyPromise(executor) {
this.then = (resolve, reject) => new
Promise(executor).then(resolve, reject);
this.catch = (resolve, reject) => new
Promise(executor).catch(reject);
}1. Eager, not lazy: Why is lazy better? Sometimes I want eager, I use promises. Sometimes I want lazy, I use a promise getter. Done. What if it was the other case, how would I turn a lazy promise into an eager one without messy code?
2. No cancellation. Events that permit cancellation are rare. Situations in which you would want to cancel something are rare. If you face these, use a promise library that does permit cancellation. Bluebird does it.
3. Never synchronous. If you want synchronous, just don't use a Promise, use a function that takes another function. I don't get the point about "callbacks to sync". Callbacks are asynchronous. "Synchronous callbacks" may have this name, but they're not actually callbacks, they're functions. A function can take another function as a parameter, that doesn't automatically make it into a "callback".
No, it's not. Promises suck, that's all, no need to spend more words on it.
You then have something like an async generator, which is like a fusion of asynchrony and sequence?
Except generators are pull not push, so instead you have a promise that accepts a function that operates on a sequence?
I don't know if this pattern is common somewhere, someone who does please explain!
I guess you could implement this in JS by having a promise-like structure which returns a tuple containing progress information AND a promise for the remainder of the computation. In a sense, this is similar to the generator approach.
One problem is if your program execs an external program. At what point should a promise kill the external process?
res=fetch('i-just-want-the-result.com')
at least when scripting with node.js
He doesn't like that the callback is called immediately - but Promises just represent a result that will be available later and do not guarantee (and should not) when the function will be called. If you want to delay some function call, do it explicitly or use a delay promise.
In my opinion, the main problem with promises is broken error handling. They don't play well with exceptions. For example:
var p = Promise(function (res, rej) {
throw new RuntimeError("System is broken");
});
This code will just ignore the error. While it is expected that the runtime error would float up and terminate the program - that is what runtime errors are made for.This also makes writing tests more difficult because tests often use exceptions to indicate failure.
I have some ideas how to fix it (neither is perfect), but the comment will become too long.
e.g. consider this python code:
def start_task():
Thread(do_task).start()
What happens if do_task throws an exception? The exception can't propagate up from start_task because start_task may have returned when the exception is thrown.At some point you have to join your threads, and you have to await/then your promises, otherwise there's no well defined place in your program for the exception to go.
Linters can help with this. tslint is one example, I believe it can give a warning for unhandled promises.
`p` will reject, any function that awaits p will reject. The error is that you've fired off an asynchronous task but you don't have any code that cares about the result of that asynchronous task (and the stack of any code that
I don't see the problem with that. Unhandled exceptions can occur at any place of your program.
I also don't see why the unhandled exception from the background thread cannot terminate main thread. Why not? That is how unhandled exceptions are supposed to work. Terminating the program is the optimal default behaviour for any error in my opinion. This way you won't miss them.
This is how unhandled promise rejections will be handled in node soon. In the browser, you can declare an event handler for unhandled promise rejections.
- return an error
- panic
What is the recommended, best, pretty, beautiful way? Return an error. Nobody uses panic.Javascript Promises are equivalent to that.
If you want to stop the main program, call process.exit(). That would be the equivalent of panic.
Chaining functions is for functional people. Go is proudly imperative.
def start_task():
thread = Thread(do_task)
thread.set_error_handler(SpecificErrorType, handler)
thread.start() function startTask() {
const promise = doTask();
promise.catch((e) => {
if (e instanceof SpecificErrorType) {
handler(e);
} else {
throw e;
}
});
}Two Erlang processes (~actors) can be linked together. If one then crashes (uncaught exception), the other one is automatically killed - unless it takes special steps to "trap exits", in which case it is sent a message describing the failure in its linked peer.
Error handling is annoying, and this is (eventually going to be) fixed in Node.js. In the browser, you can add some listeners: http://2ality.com/2016/04/unhandled-rejections.html
This will supposedly happen in a future version of node. [1]
[1]: https://nodejs.org/dist/latest-v8.x/docs/api/deprecations.ht...
No, this behavior is consistent with how asynchronous programming works in JS.
myDiv.addEventListener("click",function(event){
throw "error";
});
Nobody would expect that code to terminate a browser tab on error.> This also makes writing tests more difficult because tests often use exceptions to indicate failure.
Then use async functions, problem solved.
That is because browser environment catches all exceptions and displays them in console. So effectively they become handled exceptions.
No, it has nothing to do with the developer console. It has everything to do with the fact that async exceptions do not bubble up in the main execution thread. Promises work the exact same way since they existed as libraries long before they were included in the language. It's about consistency, nothing more, nothing less.
The receiver of the promise is the one who should handle rejection. Not the one creating the promise.
If the receiver doesn't handle it you can register global error handler and log all unhandled errors.
I started working with JS promises specifically when they were barely available in a beta runtime. It took me over a year of working with them to really get a feel for them, now it's been far longer. That's because while you can "understand" the description and use it just fine, but a deeper comprehension and intuition takes much more time. I experimented a lot and insisted on writing my own helpers from scratch, without looking up other people's code, because I wanted to get a feeling for the details.
This article seems quite artificial to me, the problems mostly made-up.
I don't see the point of the first complaint. If you don't want to start right away chain it to something that it should wait for. If it should not wait, then it can start right away. Her writes "Functions rescue us in this case because functions are lazy." which I don't quite understand: what is he running through promises if not functions? Hi "betterFetch" example mixes synchronous and promise syntax - how about using async/await if you prefer the former? I admit though I don't quite get the point of that example.
I don't understand the whole "run a promise" idea either - because you don't "run a promise", that whole notion has nothing to do with what "promise" means. Just look at the word! It represents a (wrapped) future value. Where does the idea of "running it" come from? How do you "run" a (future) value?
You have a function and it is quite easy IMO: Using a promise you chain it to whatever you want to wait for. These days you can even use semi-synchronous syntax (async/await). "Running a promise" makes no sense to me, you run functions, and I don't see where the difficulty lies here?
The second point, cancellation, has been discussed very, very thoroughly - after all, this was on the table to be standardized. One of the issues he raises is the same as point one - if you have a chain it's automatic. The main issue of cancellation is that you have zero control over the actual asynchronous operation that the promise actually stands for - because this is controlled by the OS alone! If you started I/O, what does "cancelling the promise" mean?
1. If it is still waiting: If you don't want to run something make sure the previous step returns a rejected promise. You can easily "cancel the promise". Just let your promise function check something in the parent scope (via callback or it is in its lexical scope) when its chained function starts, and if that says "you are canceled" then don't do it. You can put such a check as a standalone function anywhere in the promise chain you created, just let that "amIcancelled()" function throw or return a rejected promise. The whole chain aspect is something that the article is missing.
2. If the code is already running: you cannot cancel the actual (OS controlled) asynchronous operation, nor can you cancel a running JS function (unless you use async/await see bottom paragraph).
I agree that promises are not perfect, but async/await - not mentioned at all! - makes it a bit easier for many people - as long as they don't forget one thing: Even if your functions now look like synchronous ones there is a fundamental difference: A synchronous JS function is never interrupted by any other code. An async function is suspended and other JS code gets to run in the middle of it when it encounters an "await". This is something new first introduced by generators, before that JS functions were atomic (now some are not).
Quite a few points didn't make any sense or simply showed misunderstanding around how and why promises are what they are.
> Eager, not lazy
Why does this even matter? It's an implementation detail that optimises for performance.
The outcome, eventual resolution, is all that really matters.
> No cancellation
These are not tasks, and covered elsewhere in the thread: I/O.
> Never synchronous
Its a promise, so that doesn't really make sense to complain about.
Alas it is solved with Async/await (which is just promises under the hood.)
> then() is a mix of map() and flatMap()
This one I can concede as it would be useful to have the option, or at least have them exposed.
I suspect then was simply kept because that's what bluebird or whatever it was at the time did.
I believe he's thought a lot about this problem.
This is way too broad a claim. If I do something like:
(sleep 1 ; echo "done") &
kill $!
I'm pretty clearly "cancelling the actual (OS controlled) asynchronous operation". Now it may have done some sleeping at that point, and if you replace the sleep with something that has side-effects, then some of those side-effects may have occurred, but the operation is still being "cancelled".Obviously it's not the case that every operation can be (meaningfully) cancelled, but some can. This is even more true when you consider that when you're using JS, you're generally way above the OS level. XMLHttpRequest has an abort() method for a reason: The underlying socket request may be queued up based on your browser's connection limits and, even if it's kicked off, your browser is going to have multiple opportunities to abort the process even if none of the underlying sub-operations can be preemptively cancelled.
I had a similar experience. It took quite a while for me to stop shooting my foot. My takeaway from that experience was that, while they do have certain advantages, Promises suck. Any abstraction that is so unintuitive that it takes beginners dozens or hundreds of hours to master is probably not an abstraction worth using - especially if it is supposed to be a primary feature of the language.
Javascript can not depend on anything like that, because it's a gatekeeper language. But that is too general a phrasing.
I don't think that's quite fair. At worst promises are marginally less shitty than callback hell, so long as you don't go nuts and nest them too deeply.