10 ms·
We have a problem with promises
- jjaredsimpson 11y agoIf devs don't want to read specs and instead read blog posts then its obvious why there is a problem.
- mistercow 11y agoYour snark would be appropriate if humans were logically omniscient, but we're not. Reading and comprehending a spec does not imply grokking all of its consequences.
- johnnymonster 11y agoits interesting that when people talk about the newest feature or framework in javascript, they tend to forget about javascript's core functionality. It's always necessary to look at things like promises with javascript's core functionality in mind. Take #3 for example, which is really just a function of the core way javascript executes, taking promises aside! You would not do something like this would you in a normal day at the office? function myFunction(doSomething()){ }; so why would you do this. Promise.then(doSomething()); doSomething() gets immediately invoked when the code is evaluated. It has nothing to do with Promises. Don't forget the fundamentals!
- robmcm 11y agoThis article has a lot of ambiguity which I think adds to the confusion. For example I was looking at that question(3) thinking, does doSomethingElse() return a function, another promise, nothing? Part of the problem of having a very forgiving, lose language and API is that it leads to hidden, subtle complications, highlighted by "Advanced mistake #5: promises fall through". Here the then() method accepts anything as a return type, but will only chain a function that returns a promise, not a promise it's self. This isn't present in the MDN documentation (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...).
- vkjv 11y agoThis. That was my "answer" to the question. "Oh, if `doSomethingElse()` returns a function, it will run that function after the promise resolves successfully."
- juliangregorian 11y agoWell, not necessarily. Let's say doSomething is a closure that itself returns a function. Then it makes perfect sense to call it that way.
- LordHumungous 11y ago>If you know the answer, then congratulations: you're a promises ninja No... you know how functions work in javascript.
- simonw 11y agoUnderstanding functions in JavaScript is not enough to be able to complete the quiz. You also need to understand the somewhat surprising semantics of the Promises .then() method.
- carussell 11y agoYou don't. Just to drive this home: I've read the spec a few times when evaluating whether to integrate the concept into code I've written, soon after the pattern was introduced, and then again a year or so later. (Side note: the result of both evaluations was a "no thanks".) But at this point, I've never used or authored a Promise, ever. And before moving past the intro that contains the quiz in question, it's been over a year before I last looked at them, much less read any up-to-date spec. The quiz in the intro doesn't actually do a good job of exercising the details of `then`, because they're all trivial; there's only a single use of `then` in each example, rather than a chain of multiple ones (or a chain involving `then` and `catch` for a less useful example). You can get past the quiz in the intro by having only a general understanding of how `then` works. In fact (and I can only speak for myself here, but this is how it played out when I went through it), not having an extensive familiarity with Promises may help for the quiz, because that knowledge isn't crowding your thinking, and all you see is function calls. The author points out the third example from the quiz as particularly sticky, but it was the easiest gotcha to catch with my mostly Promise-free background, because the effect where overlooking parts that can lead you astray (because you expect idiomatic patterns in that place) is pretty totally nonexistent. It's the examples further into the article that you need to really look into and consider how `then` works. For example, in rookie mistakes #2 and #5, understanding how `undefined` is handled required backtracking to the intro where the spec is linked, in order to delve into interaction with "synchronous" return values, and `undefined` in particular. Lesson for potential interviewers: don't rely on the intro quiz in this article for your fitness function to weed out candidates, because it could mean you're optimizing for the people you don't want (like me), rather than those you do. Do make sure to focus on the examples further in that actually exercise an understanding beyond the superficial.
- davexunit 11y agoThe messes that callback/promise-heavy JavaScript make are a good example of why we need syntactic abstraction in the language. With a proper macro system, this whole async mess could be abstracted away and we could write nice linear code.
- mbrock 11y agoIn Scheme, for example, a solution would be likely to involve delimited reified continuations and not need much in the way of macros. It's not obvious to me that the problem can be solved accurately with just syntactic macros. Would the solution involve transforming code into CPS?
- davexunit 11y agoYes, delimited continuations would be a big part of the solution in Scheme. The macros would just be the sugar on top of the whole system such that you didn't have to write out all of the lambdas and prompting and aborting of the continuations. I've implemented a coroutine system this way and it works nicely.
- Too 11y agoYeah, i cant believe async support is not a required check box for programming languages nowadays. Almost all the code I write is asynchronous and it is a pita to always wrestle around different patterns exposed by various libraries, and write bloat for things that are essentially sequential just that they happen to involve a lot of io latency. No, instead we get syntax sugar for writing shorter get/set-properties.
- davexunit 11y agoThis is why I think macro systems ought to be a must-have feature in modern programming languages. There's no way that language designers can anticipate every type of convenient syntax, nor would they accept everything that we might want. Then, if they decide to add the new syntax we've been clamoring for, we have to wait for the implementation that includes it to become widely available, especially so in the case of JavaScript. We lowly programmers should be able to put on our language designer hat and extend the syntax of our languages as needed.
- STRML 11y agoGreat article; a few things I would add. I use bluebird for Promises, which is just the most fantastic Promises lib ever conceived, no joke; if you haven't used it try it. So some of these may be Bluebird-specific: 1. Don't wrap callback functions manually with `new Promise(function(resolve, reject) {...})`, just use `Promise.promisify(...)`. For one-off functions, try `Promise.fromNode(function(cb) { fs.readFile('..', cb); });`. 2. This pattern: getUserByName('nolan').then(function (user) { return getUserAccountById(user.id); }).then(function (userAccount) { // I got a user account! }); Could be: getUserByName('nolan') .get('id') .then(getUserAccountById) .then(function (userAccount) { // I got a user account! }); 3. I too used Promise.resolve().then(...) to start a lot of route handlers. Try `Promise.try(function() {...})`, which is equivalent but reduces the temptation to just stick a synchronous value in the `Promise.resolve()` just because you can. 4. `Promise#nodeify()` is super useful for creating functions that return promises or use callbacks depending on how they're called. For example: function getUserName(id, cb) { return db.getUserAsync(id).get('name') .nodeify(cb); } Is the same as: function getUserName(id, cb) { var promise = db.getUserAsync(id).get('name'); if (!cb) return promise; promise.then(function(result) { cb(null, result);}) .catch(function(e) { cb(e); }); } This is great if you want to convert a few functions you use to promises, but they're called elsewhere and expect a callback style. I'm sure there are many more. This is my bible: https://github.com/petkaantonov/bluebird/blob/master/API.md https://github.com/petkaantonov/bluebird/blob/master/API.md In short; use Promises! They are the answer for callback hell in Node. Async/await may fix more problems in the future but if you want Node/Browser compatibility and to get started right now, Promises are the best way to go.
- nolanl 11y agoBluebird is awesome. :) I especially love promisifyAll() and promisify(). The only reason I didn't include something like that in the post is that I didn't want to overwhelm newbies. I think it's already confusing enough what the difference is between q/when/RSVP/bluebird/jQuery promises/Angular promises, etc... And honestly, more and more I just use vanilla promises, or a tiny polyfill like Lie.
- ddoolin 11y ago
- falcolas 11y agoCallback heavy languages can be very challenging to think about. They require keeping quite a few disparate pieces in your mental stack all at once. Promises make this both better and worse, in my experience. In the easy cases, all you have to do is follow the promises syntax, and you can treat it as linear code. The problem arises when writing non happy path code, you not only have to think about the promises syntax, but about the abstraction it's creating over callbacks. Otherwise, you begin to miss corner cases, create incorrect promises, and in general write code which misbehaves (often silently). Promises are powerful abstractions, but like all abstractions, they have their leaks. Also like all other abstractions, there are places where you have to fully comprehend the mechanisms underneath them to use them properly.
- nolanl 11y agoVery true. And as I mention in the article, ES7 async/await should help fix that: http://pouchdb.com/2015/03/05/taming-the-async-beast-with-es7.html http://pouchdb.com/2015/03/05/taming-the-async-beast-with-es...
- inglor 11y agoWell, `async/await` introduces its own set of problems - I've answered questions on SO before where the OP didn't understand why they got degraded performance when they were doing `await` in a for loop instead of doing things concurrently. At the end of the day you still have to understand promises in order to use `async/await`
- nolanl 11y agoThat's a fair point, but I think the name itself ("await") tends to communicate the the operation might take a long time. Compare that to multithreaded languages like Java, where it's not obvious that FileInputStream/HttpClient/etc. could take a long time! Anyway, the big benefit of async/await, to me, is that you really DON'T have to understand promises. :) For instance, at some point we will probably add ES7 examples to the PouchDB API docs (e.g. here: http://pouchdb.com/api.html#create_document http://pouchdb.com/api.html#create_document), and you can imagine that, from the user's perspective, the word "await" will just be part of the API (e.g. `var doc = await db.get('id')`), and beyond that, they won't have to learn anything about promises or async or callbacks or any of that.
- greggman 11y agoI would argue that `Promise.all` is not the equivalent of `forEach` because Promise.all will execute everything immediately, maybe all will fail, maybe 1 will fail, eventually you'll have your then or your catch called but 100 of your actions will have an attempt to be executed. Compare that to forEach, if one fails and you throw the rest don't get executed. I suppose if whatever you're doing for each thing is async then they are equivalent equivalent but in general forEach has different sementics than Promise.all
- MasterScrat 11y agoNow I wonder what would be the good way to make a forEach that actually returns when all elements have been executed?
- dcherman 11y agoBluebird has this built in. For a quick userscript where I needed something similar, I created this which works ( warning: no unit tests, but it works for me ) https://gist.github.com/dcherman/4dfba0d72c008ee5b59d https://gist.github.com/dcherman/4dfba0d72c008ee5b59d It's a reduce function, but it could work as a forEach as well.
- inglor 11y agoBluebird has a `Promise.each` which does this. Also, you can use `thenable chaining` - make an initial promise and then chain it on each iteration - it becomes very similar to a for loop. I can add an example if you'd like.
- nolanl 11y agoYou're right; I'm playing a bit fast and loose with the definition of "equivalent." However, I think it's what most people want when they reach for the forEach(). In that "Promise protips" gist (https://gist.github.com/nolanlawson/6ce81186421d2fa109a4 https://gist.github.com/nolanlawson/6ce81186421d2fa109a4), I have an example of something closer to what you describe: the sequentialize() function will execute one promise after the other, and fail as soon as a single one fails. However, if you need to actually run multiple promises in parallel, but cancel them as soon as a single one fails, then you might want something more advanced than promises, like RactiveJS (https://github.com/ractivejs/ractive https://github.com/ractivejs/ractive) or RxJS (https://github.com/Reactive-Extensions/RxJS https://github.com/Reactive-Extensions/RxJS).
- arxpoetica 11y agoI made the transition from callbacks to generators/iterators recently, and I'm really enjoying the yield/next foo. Promises just never really spoke to me. Not certain why I always felt reticent to use them.
- naturalethic 11y agoYield works with promises too.
- xtrumanx 11y agoWhat's so bad about using deferred? In one case I call the resolve or reject parameters and in the other I call a resolve or reject properties on the deferred object. Not much of a difference to me. I learned about deferred a few years ago and kinda stuck with it. It's all over my code base and he didn't really justify why I should go about changing it. The only thing I can reason I can think of is using his recommendation follows the ES6 spec which doesn't matter to me that much for now.
- loganfsmyth 11y agoThe main thing in my mind is that it means your code could have cases where it throws synchronously, and cases where the promise rejects with an error. One place this is mentioned is: https://github.com/petkaantonov/bluebird/wiki/Promise-anti-patterns#the-deferred-anti-pattern https://github.com/petkaantonov/bluebird/wiki/Promise-anti-p...
- esailija 11y agoIt's just mountains of unnecessary extra code that is error prone because you have to manually wire it. When you chain a promise or promisify a callback instead of using a deferred or the promise constructor (both are just as evil here), the wiring is done implicitly and there is a lot less code. Edit: What I mean by "wiring" is that you need to make sure that in all successful code paths `deferred.resolve` is called and that in all erroneous code paths (not always a single try catch either) `deferred.reject` is called.
- Lio 11y agoThe main issue I've found is with the way that deferreds handle then failures and how "then" works. According to the Promises/A+ spec, then should always return a new promise. With the jQuery deferred.then() that's not the case. In practice this means that it's harder to recover from failures as you're processing your results because your all your onRejected callbacks are attached to the same deferred and so will all fire one after another. If you're just dealing with one asynchronous event, for example an AJAX call, that might not be a problem but it can cause problems debugging more complex code if you're not very careful. E.g. (Get request) -then-> (onGetSuccessProcessor, onGetReject) -then-> (onProcessSuccess, onProcessingReject) If the get request fails the onProcessingReject will also execute even though we haven't run the onGetSuccessProcessor callback. In Promises/A+ code you can return a new resolved promise from your onRejected callback and the next "then" will fire it's onResolve callback instead of onRejected. Hope that helps. (it's a lot easier to draw as a petri net than to explain in a comment on HN :)
- nolanl 11y agoIf anyone's confused by the 4 "puzzles," I whipped up a JSBin to demonstrate: http://jsbin.com/tuqukakawo/1/edit?js,console,output http://jsbin.com/tuqukakawo/1/edit?js,console,output
- eevilspock 11y agoWhat if the doSomethingElse in #3 returns a function that is equivalent to the doSomethingElse of #1? Shouldn't #3 then be identical to #1?
- nolanl 11y agoYes, but in most code I've seen, that's a typo rather than someone intentionally writing a function that returns a function. You can definitely do it!
- janfoeh 11y agoI am stumbling over this: > Just remember: any code that might throw synchronously is a good candidate for a nearly-impossible-to-debug swallowed error somewhere down the line. Why would a synchronously thrown error be swallowed, and why would I not just `try { } catch` here?
- nolanl 11y agonaivePromiseAPI() { if (foo) { throw new Error('this will get swallowed!'); } return somePromise(); } Instead you want this: cleverPromiseAPI() { return Promise.resolve().then(function () { if (foo) { throw new Error('this won't get swallowed!'); } return somePromise(); }); } Because if the client of the API does something like this: $('.my-button').on('click', function () { cleverPromiseAPI().catch(console.log.bind(console)); }); Then the client might expect the error to get console.logged, but actually it won't in the naive case. That's because the error was thrown synchronously and thus bubbled up to the click handler, instead of being caught inside of the promise chain.
- janfoeh 11y agoAh, I get it now, thanks! I missed the part about doing that for synchronous code in promise-returning APIs. I had a moment of stupidity and first somehow understood that to be a general suggestion for error handling.
- adhambadr 11y agohahahahaha this one cracked me up "Writing code without a stack is a lot like driving a car without a brake pedal: you don't realize how badly you need it, until you reach for it and it's not there."
- Kenji 11y agoI didn't really understand that one. First of all, I don't see why a stack really is that indispensable, and secondly, JavaScript does actually have a stack so I don't know how he thinks it's operating stackless - where do the local variables go?
- nolanl 11y agoThis is what I mean: doSomething(function (err) { doSomethingElse(function (err) { doYetAnotherThing(function (err) { throw new Error( "I don't bubble up to the top of the stack, " + "because there is no stack!"); }); }); }); BTW a great trick is to tick the little "Async" box in the Chrome Dev Tools, so you can get a full stacktrace showing exactly where you came from. It works for both callbacks and promises: http://www.html5rocks.com/en/tutorials/developertools/async-call-stack/ http://www.html5rocks.com/en/tutorials/developertools/async-...
- jerf 11y agoIn the classic, but widely misunderstood, "Goto Considered Harmful", Dijkstra's point is basically that structure programming is good because it produces a stack and an instruction pointer, which contains an immense quantity of useful information when a crash occurs. Goto is bad in this paper because when it is used as the only control flow construct, in a way difficult for a modern programmer to understand in a world where structured programming won so thoroughly that every language is structured, it produces no stack trace. You get a "Crash On Line 28430", with no clue how you got there. Naive event-based code is somewhat like a goto, in that every time an event comes in, you get a new stack constructed for that event. While you then get a stack if you call functions from the initial handler, every event that comes in whacks the stack afresh. So it isn't "the same as" a goto, but it does hurt the stack. Instead of a stack frame that represents the whole of the program, you get a series of "island" stack frames. In some sense, this is the most fundamental reason why event-based programming is so painful, and it's the problem that all these multitudinous programming "async" techniques are trying to get around. Promises, for instance, can be looked at as a partial way of having a sort of stack around a particular sequence of operations that may be interrupted in arbitrary locations by this event-induced stack discarding. In this sense they can also be viewed as an inner-platform problem; where the language has "try/catch", for loops, etc, promises end up trying to recreate all that in a second layer above the bottom layer, with all the corresponding complexity of embedding a language in the language. If you then end up asking, "Well, what if we just didn't discard the stack in the first place?", you end up in Erlang/Go/Haskell/Scala land.
- embwbam 11y agoTypescript or Flow will catch many of these errors. Highly recommended!
- deleted 11y ago[deleted]
- tel 11y agoAnd this is why types are nice. Along with limited overloading.
- tel 11y agoRe "Advanced Mistake #4", do Javascript programmers not know about tuples? Does nobody write functions with signatures like pair(pa: Promise<A>, pb: (a: A) => Promise<B>): Promise<{fst: A, snd: B}> { return pa.then(function (a) { return pb(a).map(function (b) { return {fst: a, snd: b} }) }) } It's a super reusable little chunk of code.
- phs2501 11y agoIt is, but it only works for pairs and then you wind up with the relatively nonsensical 'fst' and 'snd' names. My preference would be: let user = await getUserByName('nolan'); let userAccount = await getUserAccountById(user.id); // stuff with user and userAccount But I have long since decided that I am never writing ES5 again, and since I therefore need Babel anyways I may as well turn 'stage: 1' on and use async/await. It's basically do-notation for promises.
- tel 11y agoSo alpha convert your names to whatever you like. I don't know how to do it namelessly in Javascript without Chuch encoding the pair. You can go much further than this. The following operation ap<A, B>(pf: Promise<(a: A) => B>): (pa: Promise<A>) => Promise<B> { return (pa: Promise<A>) => pf.then((f) => pa.then((a) => Promise.resolve(f(a)))) } can be used to extend the previous idea as far as you like.
- phs2501 11y agoI'm having trouble grokking whatever type-added syntax you're using for Javascript here. I'm guessing it's: function ap(pf) { return (pa) => pf.then((f) => pa.then((a) => Promise.resolve(f(a)))) } Still, if I wanted to go full-hog static-typed functional I doubt I'd be using straight Javascript, I'd probably try one of the various Haskell-alike to JS converters. Promises with async/await syntax seems like the most practical of the various solutions whilst still feeling like Javascript. (Besides, I prefer dynamic typing and I find points-free style in Haskell to be obfuscating rather than clarifying, so it seems unlikely we're going to have the same opinion of the best way to accomplish things here. :)
- tlrobinson 11y agoAs soon as you can reasonably introduce an ES6/7 transpiler into your toolchain you should start using ES7's async/await or equivalently ES6's generators + a coroutine library function like Bluebird.coroutine, Q.async, co, or Task.js. It solves basically all of the problems mentioned in this article.
- adrusi 11y agoThe real confusion here is dynamic typing. It's weird that the parameter to .then() can return any value, but if it's a promise there's special behavior. And for no good reason either p.then(r => r + 5); Is just a bit of sugar for p.then(r => Promise.resolve(r + 5)); And then the type signatures are easy to reason about. The dynamic typing also risks introducing hard to find bugs. Suppose you have an heterogeneous array `things` which might contain numbers and promises which will resolve to numbers. p.then(r => things[r]) .then(thing => selectedThings.push(thing)); You might intend `selectedThings` to also contain either numbers or promises that resolve to numbers, ordered deterministically as a function of `p`, but instead it will contain only numbers and its order is nondeterministic (it depends on `p` and all of the promise members of `things` that `p` ever signals).
- noelwelsh 11y agoThis is exactly right. If the spec defined an API with simple semantics half these problems wouldn't exist. As it is `then` is overloaded to perform numerous distinct operations. This means it can't report any useful errors, because any value passed to `then` is valid. There was even discussion about this when the spec was being written, but it was decided the current semantics are more in keeping with Javascript. Unfortunately the spec authors were correct.
- Guillaume86 11y agoI don't agree with your first remark: what do you expect to be returned when you return a value not wrapped in a promise? The ambiguity is actually the auto unwrapping of promises for subsequent then calls but I've yet to see a case where it's a problem (it's actually what makes the API clean looking when used). For your last snippet, I don't see an array so I don't know what ordering you want to preserve (p always returns the same value so r will always be the same), if it's actually inside a loop, you should use Promise.all or Promise.map.
- tel 11y agoHe's stating the problem is dynamic typing... so the expectation would be a type error (!).
- neumino 11y agoGosh, adding a `.catch(console.log.bind(console))` is just insane. If you have a library that will return non operational error, just remove it from your project. If your code throws when it is not expected to, fix it. This is like saying put a `try/catch(ignore)` everywhere. Seriously.
- ben336 11y agoLogging errors to the console can be helpful to find them during debugging/development (as opposed to having them fail silently). The goal isn't to leave errors in, its to expose them when something changes to make them happen so that you can do the 2 things above.
- esailija 11y agoYou need to configure or change your promise implementation if uncaught errors fail silently. In any case, logging error gives you no information anyway. Just try running: `console.log(new Error("..."))`. So at the very least you should do `catch(function(e){console.log(e.stack);})`
- neumino 11y agoYou can just crash (and still see the stacktrace) and automatically restart. You shouldn't leave these `catch/ignore` in your code in prod, this is a pretty good way to get undefined behaviors and maybe corrupted data.
- Too 11y agoYeah. In chrome if you enable "break on exceptions" it will also break on unhandled promise rejections. This will take care of all your debugging needs.
- nolanl 11y agoYep, Chrome is pretty great about printing unhandled promise rejections to the console. However, I just tested Firefox and Safari, and neither of them will warn you in that case - not to mention older browsers like Android <4.4 and IE <=11, which don't even have native promises! So if you're ever trying to debug promises in a non-Chrome browser, you may find my "insane" advice pretty helpful. :)
- tumbling_stone 11y agoThe most comprehensive and foolproof way is to grab the spec, read the algorithm and fiddle around a day. Sadly this is the only way of fully understand promises, promises already put a lot of cognitive load on your brain when you're using them, so having any other abstractions of your own (for remembering how promises work) is bad idea. IMO you're better off investing a large continuous block of time for understanding promises rather than reading some article here and there.
- vkjv 11y ago> Rookie mistake #3: forgetting to add .catch() I disagree with this advice. I usually refer to this as a "Pokémon Catch" (Gotta' Catch 'Em All!). Never catch a promise that you are un-able to handle. It's better to use a Promise library that supports debugging possibly unhandled exceptions (e.g., Bluebird). > Rookie mistake #4: using "deferred" This one is interesting because it's actually used in the Q library documentation for a way to `denodeify` or `promisify` a function. You definitely shouldn't do this if it's already a promise, but outside of that, it's more of a gray area. I tend to recommend using libraries to convert callback functions to promises.
- inglor 11y agoYes, adding `catch` is mostly an anti-pattern today, most promise libraries as well as native promises on most platforms will detect unhandled rejections for you if you know the hooks. Bluebird is particularly good at this, but Q, When, RSVP and native promises all do this.
- phs2501 11y agoThe problem with "var d = defer(); ...; return d.promise;" vs "return new Promise((resolve, reject) => {...});" is that the former does not protect you from synchronous throws. In the latter (at least with every promise library and native promise implementation I've seen) if the code in '...' throws, it rejects the promise with the thrown error. In the former, it throws synchronously, which especially in node is almost never what you want. Besides, if you need to promisify a node-like function you should just use Bluebird's promisification features; no point in doing it yourself. The fact that Bluebird is also the most debuggable and performant Promise implementation (including native) is just icing on the cake.
- mweibel 11y agoVery nice article, will keep that in mind when trying to help others with promises. Also helped me to re-understand some things :) One thing though, Advanced mistake #4, is in my opinion good answer, the Q library however gives a (afaik) non-a+-standard way of doing that which I like: from: getUserByName('nolan').then(function (user) { return getUserAccountById(user.id); }).then(function (userAccount) { // dangit, I need the "user" object too! }); to: getUserByName('nolan').then(function (user) { return [user, getUserAccountById(user.id)]; }).spread(function (user, userAccount) { // I do have the user object here });
- ThrustVectoring 11y agoPuzzle number 3 doesn't have a complete explanation. DoSomethingElse() can return a function, which is then evaluated with the result of the first promise as an argument.
- deleted 11y ago[deleted]