I have yet to see a convincing argument that this feature is necessary or even helpful beyond one-liners. The Q promises API, to me, is the right way to reason about asynchrony. Once you understand closures and first class functions, so much about complex asynchronous flows (e.g. multiple concurrent calls via Q.all, multiple "returns" via callback arguments) become so simple. The "tons of libraries" argument doesn't make tons of sense either. I've done a lot of async and I've never needed anything beyond Q that I can recall.
This feels like a step toward added confusion rather than language unity. Much like the majority of of ES6 feels to me: bolted-on features from popular synchronous languages that themselves are only now adding features like lambdas.
I don't want to write faux-event-driven code that hides the eventedness beneath native abstractions. And I definitely don't want to work with developers, new and old, trying to learn the language and whether/when they should use async/await, promises, or fall back to an es5 library or on* event handlers. I want developers who grok functional event-driven code with contextual closures.
Allow me to make both a theoretical and practical argument.
It's been said that "callbacks are imperative; promises are functional". It's true. Furthermore, callbacks structure control flow and promises structure data flow. Instruction scheduling is an explicit sequence with callbacks (well, assuming the API calls you back precisely once), but scheduling is an implicit topological sort of the directed acyclic dependency graph of promises.
Sometimes, when it comes to side effects, implicit scheduling is less than ideal. You need specific things to happen in a specific order. The solution is to introduce data dependencies to force a particular schedule. This is precisely what is done by monads in Haskell. However, unlike Haskell, JavaScript doesn't have "do" syntax, so there's no convenient notation for a nested chain of bindings.
The result of not having monadic binding syntax is that you wind up with some funky nested chain of getY(x).then(y => y.then(z => z.then(.... OR you have to declare variables up top, flatten your then blocks in to a promise chain, and often ignore intermediate values:
let y, z;
getY(x)
.then(returnedY => { y = returnedY; return zPromise(); )
.then(returnedZ => { z = returnedZ; return ......; }
.then(......another side effect.....)
.then(_ => f(y, z))
async/await neatly eliminates this problem by reusing the traditional coupling of data flow dependencies with first-order control flow dependencies.
Practically:
let y = await getY(x);
let z = await getZ(y);
......another side effect....
return f(y, z)
Not the cleanest, but you preserve the isolation without relying on polluted variables bleeding into function scope. In addition, one can do proper error handling via .catch as opposed to the brute force unnecessary catchall that is a try-catch.
1) Yes, you can restructure/destructure variables at each step, but you're just unifying the control flow and data flow by hand. Yet another flavor of human compiler.
2) catch's entire purpose is to be a catch-all for unexpected errors. For expected errors, it's better to have explicit error values. Bluebird.js at least lets you differentiate .catch from .error, where .error handles the explicit error values generated from traditional Node.js callbacks.
> It's been said that "callbacks are imperative; promises are functional". It's true. Furthermore, callbacks structure control flow and promises structure data flow.
Do you have any good escapes l examples of further reading on this? I'm not sure I agree with this statement, or perhaps don't fully understand it. It seems like both cases boil down to functions as data (function pointers essentially) that are called at the end of some event or chain of events.
To me, they seem to be the exact same thing but worth promises being much more human readable.
To give a brief "explanation", look at the type signature of a callback-based API:
readThisFile: string -> (string -> void) -> void
Usage:
readThisFile('foo.txt', result => ...)
You have a double void here. Which is the hand-wavy justification as to why callbacks don't compose. On the other hand, a promise's value is Promise<actualTypeOfValue>, which you can pass around as data without asking the other end what it wants to do with the value (aka decoupling). You do have the imperative action at the top to kick the whole chain into motion, but the intermediate stuff are most of the time side-effect-free.
Hopefully that was clear enough to see that passing actions around is akin to controlling the flow with e.g. if-else, and that passing data around is declaring how your tubes/rail tracks/whatever analogy for monads are constructed, aka data flow.
99.9% of all JavaScript is handling state and events in a user interface (web page).
Ex: You have two buttons, and when both of them has been pressed, a image of a cat is displayed.
>> functional event-driven code with contextual closures
This paradigm works very well on the server side too.
Ex: A HTTP requests makes a query to a database server, then returns the result to the browser
This gives parallelism without the complexity of threads.
This way, if one response was much quicker than the other, you could begin sending it through the `res.json()` portion immediately, instead of waiting for both responses to return before continuing.
null, {
facebook: { // facebook data },
twitter: { // twitter data }
}
Sure it suffers from a third party dependency and I would argue its slightly less concise, but we're getting to the point of opinionated API's just like some developers prefer promises, some will prefer async/await, others will continue with vanilla callbacks.
This is not the same at all. Any error in any of the callbacks is not catchable with one catch statement (you need separate try catch statement in every one of your callbacks). With async/await and promises, you only need one catch statement or one .catch method in one place because of proper error propagation and composition.
How common are dozens/hundreds of try-catch blocks in javascript code? Developers have long ago stopped caring about uncaught errors because it doesn't matter in the code they are writing, they catch every error they need to and leave the chips to fall where they may.
In the same vein async.js will be catching all errors passed to done(err, response) and in the case of async.series() and similar calls, will stop the execution of concurrent actions as at the first error that is caught.
Therefore I think its completely valid, because even though our examples are different, to most developers it doesn't even matter.
I think you have a completely valid point, and it's probably a better world where we have proper error propagation and composition, just the reality of the situation is what it is.
> How common are dozens/hundreds of try-catch blocks in javascript code
Nobody who realized the fragility of async/callbacks and understanding what it takes with them to make at least a passably robust program would stay with async, they would quickly switch to promises/fibers/equivalent or stop using node. Although I have seen projects on github that use try-catch blocks properly with callbacks/async and I have to wonder what made them think that there isn't a better way.
And again, async does not catch any errors, it only forwards error callbacks based on some very loose convention which isn't even honored in core node apis. Just because most node (or rather callback/async) users are clueless about error handling doesn't make the examples equivalent.
Promise.all doesn't "run" anything. Promises "run" the very moment they come into existence. Promise.all simply tells you if and when all of them are settled. It depends on your Promises whether they are doing something outside the single threaded event loop or not for them to be parallel or not.
>Promise.all doesn't "run" anything. Promises "run" the very moment they come into existence.
Yeah, I know that. I don't mean .all schedules them to run (e.g. like doing thread.start()), of course they auto start.
By run I mean that inside the .all implementation syncs with you about their completion ("tells you when all of them are settled").
>It depends on your Promises whether they are doing something outside the single threaded event loop or not for them to be parallel or not.
That's not the point though, which was the parent's assertion about whether the first to resolve will escape .all and go into the .map, which I don't think is the case.
Yes. They will be sent into map immediately, and Promise.all() will return a Promise which will settle when the mapped array is all settled (or one errors, but that's not really relevant to the question). We're really just awaiting the promise returned from Promise.all(), but mapping the original fetches into a "new" set of promises.
I'm sorry, but I don't see why/how they would be sent to `map` immediately, given where the parenthesis are in the above code snippet. `Promise.prototype.map` isn't a builtin function is it? If the `map` call was inside the invocation of `Promise.all` then I could see it, but perhaps I am missing something.
I will admit that I hadn't realized async worked on Promises. This is definitely a very clean solution, though not one that can be seemingly achieved without Promise.all.
There is, of course, no library needed to use pure promises for the same functionality, though.
My point is that all of this is possible without async/await, which to me are abstractions that cloud the landscape, obscuring the real evented nature of JavaScript that makes async programming so easy, IMO.
I'd say a bad programmer can produce shit regardless of the tools he is given. What async/await does is make complex async code much easier to read and maintain. Of course, you have all the expressive power of callbacks and promises at your disposal too. That's one of the great things about it. It's implemented on top of these constructs and they remain accessible.
The short answer is that async/await is to promises what loops were to if/goto. It's "just" syntactic sugar, but once you start using it, the code is much easier to write, and its structure is more obvious at a glance. It does not change the way you reason about asynchrony, because the underlying model is still the same. And you still need to understand that underneath, it's all promises and continuations. But there are very tangible benefits to be had from this kind of syntactic sugar.
This is speaking from personal experience, albeit not in JS, but rather .NET. However, the story there is similar - .NET used callback-based continuations first (the Begin... pattern), then added promises (Tasks) in .NET 4.0, then finally async/await syntactic sugar in C# 5. We introduced tasks to our codebase pretty soon, but had to avoid await for a while because of the requirement for the code to be compilable with C# 4. When we finally dropped that requirement a couple of years later, and started moving to await, code became much simpler and cleaner as a result, and new code is also much easier to write, with fewer silly mistakes (like "I forgot to check for errors").
> The "tons of libraries" argument doesn't make tons of sense either.
It absolutely does make sense. How many "class builders" have been written in the JS community since ES6 got classes ? That's right. Now you don't have to rely on co/thunk and what not to write readable async logic. Promises are still callbacks wrapped in objects. Now the consumer of a function that returns a promise doesn't have to deal directly with the promise anymore.
This feature is something JS should have had a long time ago, no question.
> This feels like a step toward added confusion rather than language unity.
Things like prototypical inheritance are the biggest source of confusion, along with "this" behavior.
> beneath native abstractions
The correct terminology here would be syntactic sugar.
Look you might not like this, but what is better ? community fragmentation with transpilers and what not, or a single language that answers most of the needs of the community ? libraries that handle complex abstractions or documented syntax that allows getting rid of these poorly documented and poorly maintained libraries? I don't want to use generators to mimic corroutines and depend on co/thunkify to write readable async code. I don't want to use 3rd party language X or Z that will get me the syntax I want, I want everybody to use the same language without obscure patterns.
To correct this a little, the proper way to catch with promises is .catch, not with a try-catch inside a .then - that is an antipattern. The promise examples as written are not proper promise usage.
I'm also in the boat of confused people of why is async-await requested by a good portion of the js community - wrapping the block within the function in a try-catch seems like a bad idea, a brute force mechanism. IMO this is language bloat, and does not really solve problems we have while introducing a more expensive vector for managing flow (try-catches).
One missed point about the .catch with promises is that it allows you to branch flow discretely - it is not a perfect solution for branching flow (it is awkward when branching due to more than just a binary outcome), but with await, one might end up writing repetitive code for certain flows where one expects common calls to be made downstream in async data fetching flows.
As I wrote in depends whether you can survive and continue from an exception inside the .then to the next step or not -- not about how the promise would handle an exception, but whether you want it to handle it or you want to survive and handle it yourself.
>The Q promises API, to me, is the right way to reason about asynchrony. Once you understand closures and first class functions, so much about complex asynchronous flows (e.g. multiple concurrent calls via Q.all, multiple "returns" via callback arguments) become so simple.
Being simple doesn't make them optimal.
Promises are a clutch for lack of a built-in language feature, and async/await is that.
They add extra visual clutter, they can eat exceptions [1], they break the intuitive flow of code, etc.
[1] Yes, only if they are used badly. That's kind of the point. C is an excellent language too if everybody never uses it badly. The thing is people do, and a feature/language that doesn't need a corner case or mental model to keep in mind is better than one that does in that regard.
I think anyone with a fair amount of experience in complex asynchronous programs can agree with you. But most people don't have that experience and things look very different to them. They don't understand yet why their sequential database queries do not matter, they don't understand that it's not where the complexity is and that seeking familiarity and "prettifying" simple code with syntactic sugar is not going to make it simpler or easier to understand and maintain. They will have to learn it the hard way.
I agree with you, I feel this way about async/await and felt similar about ES6 classes ans various other features. Perhaps one advantage is it makes the language "look" more appealing to beginners, almost all of whom are familiar with object-oriented programming and imperative programming.
In the end, as you pointed out, these construct may work against developers in large/concurrent codebases and is an undoing of the simplicity of Javascript, one of its original strength. On the other hand we can't really say this was not coming, Javascript being a single target language with many stakeholders around it.
Maybe what's needed is to stop teaching object-oriented/imperative programming as the main paradigm. But well before that happens, hopefully WebAssembly will create a evolutionary market for languages vs. a language by committee.
Beginners will have to learn more of the language, which makes it less useful for beginners. And the larger the surface area of the syntax, the larger the amount that everyone on the team must know and the probability someone will screw something up.
C++0x started adding the kitchen sink and that's when C++ jumped the shark. JS seems to be headed in the same direction...
I use plain old promises by default, but the first time I have to think about which promise results are in scope within nested then() calls, I rewrite it with async/await to eliminate that cognitive load entirely.
You'd like Tcl and various Perl libraries. Mojo (Perl event loop among other things) in particular has a very nice, straightforward event flow quite reminiscent of Q.
async/await looks nice but doesn't scale. As soon as you have to do something other than wait for a callback it falls apart.
Maybe if the only thing you do is Ajax. Writing a server, a web crawler, a P2P protocol, heterogeneous OpenCL computations, etc, and you're going to need something a little more heavy-duty.
I'm writing a p2p protocol and you are wrong. Async/await is compatible with promises and the tooling around them, but for 90% of cases it is much more understandable.
This feels like a step toward added confusion rather than language unity. Much like the majority of of ES6 feels to me: bolted-on features from popular synchronous languages that themselves are only now adding features like lambdas.
I don't want to write faux-event-driven code that hides the eventedness beneath native abstractions. And I definitely don't want to work with developers, new and old, trying to learn the language and whether/when they should use async/await, promises, or fall back to an es5 library or on* event handlers. I want developers who grok functional event-driven code with contextual closures.