6 ms·
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
by andrewstuart2 10y ago
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.
- tanto 10y ago> (e.g. multiple concurrent calls via Q.all, multiple "returns" via callback arguments) How about: async function main () { try { const responses = await Promise.all([ fetch('https://api.github.com/orgs/facebook'), fetch('https://api.github.com/orgs/facebook') ]); const jsons = await Promise.all(responses.map(res => res.json())) } catch (err) { console.log(err) } } I think this is pretty clear and not needing any library for this is quite nice.
- dvlsg 10y agoJust to be picky, but I believe you would be better off with this: async function main () { try { const jsons = await Promise.all([ fetch('https://api.github.com/orgs/facebook'), fetch('https://api.github.com/orgs/facebook') ]).map(promise => promise.then(res => res.json())); } catch (err) { console.log(err) } } 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.
- coldtea 10y agoWould it? IIRC Promise.all will run the two responses in parallel, but will not send the first into the map before both are finished.
- awalGarg 10y agoPromise.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.
- girvo 10y ago> Promises "run" the very moment they come into existence Which is why I'm a massive fan of Futures from ramda-fantasy[0] instead of Promises for certain tasks! [0] https://github.com/ramda/ramda-fantasy/blob/master/docs/Future.md https://github.com/ramda/ramda-fantasy/blob/master/docs/Futu...
- coldtea 10y ago>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.
- dvlsg 10y agoYes. 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.
- andrewstuart2 10y agoI 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.
- niagr 10y agoI'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.
- PeCaN 10y agoYou'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.
- coldtea 10y ago>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. Waiting for a callback is what you do 90% of time. For the rest, you can always combine async/wait with Promises...
- PeCaN 10y agoMaybe 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.
- norman784 10y agoThe think about using async/await its to write less code, make it much simple and makes also easy to try/catch errors. The think I don't like about promises its that you need to write a lot of lines that you could avoid with async/await, i.e: new Promise(function(success, failure){ db.fetch('sql...', function(err, data){ if (err) failure(err) else success(data) }) }) VS var data = await db.fetch('sql...') Also if you concatenate with then promise.then(function(done, args){ try { db.fetch('sql...', done) } catch(e) {} }) .then(function(done, args){ try { db.fetch('sql2... {args}', done) } catch(e) {} }) .then.... VS try { var data1 = await db.fetch('sql1...') var data2 = await db.fetch('sql2... {data1}') } catch(e) {} btw its just pseudocode but you get the main idea
- Bahamut 10y agoTo 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.
- coldtea 10y ago>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. Depends whether you can survive and continue from an exception inside the .then to the next step or not.
- 10y ago
- brandonbloom 10y agoAllow 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)
- andrewstuart2 10y ago> 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.
- chenglou 10y agoTo 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.
- coldtea 10y ago>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.
- spriggan3 10y ago> 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.
- tarikjn 10y agoI 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.
- EGreg 10y agoBeginners 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...
- natrius 10y agoI 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.
- zzzcpan 10y agoI 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.
- int_19h 10y agoThe 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").