Reflecting on what was so tricky about cancelable Promises, embracing functional purity as a solution So… . And so, the community is left once again wondering how newer, Promise-based apis like can possibly move forward. Why has this been such a blocker? And what can we do? “cancelable Promises” have been, well, canceled Javascript fetch Working with the future in the present Let’s start with a quick recap of the problem space. We could talk about the “event loop” in Javascript here… but for our purposes let’s just reflect on the fact that all the code in our applications executes in a specific order of operation: inside-to-outside, first line to last. Anything that takes time to accomplish “blocks” the rest of the application until it’s complete. Obviously, this makes activating and responding to truly asynchronous actions a bit tricky. The first and most natural pattern that can help handle this in user-level code is callbacks: you pass some function function that you want with a result once some other interface declares that a result is available. There’s nothing even inherently asynchronous about this pattern: it’s just basic higher-order functions at play: another to be called It’s worth noting that, non-blocking asynchronous behavior is almost impossible to create or imagine in javascript without using special, language-level functions that provide support for it. So let’s quickly think through one of the simplest ones that the language (at least in a browser) offers: . actual setTimeout //setTimeout :: Function -> Int -> a _setTimeout(x=>console.log(x), 700, 5);_ In this case, we just specify a callback function, an amount of time to wait before calling it, in milliseconds, and, optionally, the value to call the callback with. Given all this, the language will wait the appropriate amount of time then then call the function with the value. That’s it. Basic. There’s no polyfill for non-blocking setTimeout that I know of: either the language can do that (that is: wait, without blocking the execution of any subsequent lines of code) or it can’t. But one structural limitation with is the “dead-end” nature of its API: the side-effecting callback is not particularly extensible. That is, if you want to extend and chain further computations onto the result of the callback, you can’t do so defining the core callback operation itself. You’d have to just get everything you want to have happen ready and all packaged up inside the callback in the first place, handing it off to to be executed. Now, the callback be made to trigger other functions that are already defined in an outer scope… or even to schedule them to run later using another . But the fact remains that the actual return value returned from the callback doesn’t really GO anywhere: setTimeout after before setTimeout could setTimeout setTimeout(x=>x, 700, 5); Our callback function there, in practice a 5 as its result, but it’s basically pointless: the callback is defined synchronously (in the present) but it asynchronously (in the ). And so there’s no place for the “5” to go: nothing else is or can be defined for it to feed into. Nothing “else” exists in its future timeframe to listen to it. x=>x returns runs future Can we introduce this concept of “future listening”? Certainly! And that’s how we get Promises. Promises are like a representation of a that exist in the future… and thus provide you with an intelligible way to code against those missing values in the present. They do this by sort of “boxing up” a future event: containing an eventual explosion, and state-fully storing its result. value will Thus, when you construct a Promise, you define a function that internally executes something like a operation but then also specifies how to that result by calling one of two specialized functional handlers: resolve or reject. Like this: setTimeout capture Hopefully I don’t need to go into the API of Promises too much. Instead, let’s call out a few key things that will quickly become relevant to our discussion of cancelation: The constructor function (the one that’s passed the special resolve/reject arguments that control the Promise’s internal state) didn’t actually return anything (so, implicitly, it returns ). This is normal for promises just like it was for . Even if it does return something, it doesn’t . returns, well, a Promise: not whatever arbitrary result the constructor might have returned. undefined setTimeout matter new Promise As we said, the constructor function is executed immediately. If you immediately chain a method onto a promise, the result is a new, Promise whose eventual state depends on the outcome of the first. .then() derivative This second, derivative Promise has no (and really, any) hook back “into,” or control over, the original promise we created using the constructor! It simply derives its own “resolve” execution from the outcome of the former one. This channel of communication has room for only one thing, and in one direction: the value passed into the original resolve callback function, which is then farmed out to whatever function is attached to the promise using . should not have .then() I lied a bit in #3 of course: there IS another channel of communication: / But rejected Promises don’t really change the key details of the story that much: if an operation rejects, that just feeds a value into the handler of the next operation. Same as resolve did. And, same as resolve, it’s a function called with just a single value. There’s not a lot of room for extra signals in that pipeline! And that’s actually a very good thing, because we’re already dealing with a complex construct that we’re trying to keep simple & concrete. tiny reject .catch() reject .catch() Forestalling the future But now we get to the meat of the matter: cancelation. What if we decide at some point that we don’t care about the original operation while we’re still in the middle of it? We’d expect two critical things to happen: whatever operation was asynchronously generating a result should stop, freeing up any resources it was consuming none of the side-effects that depend on that result (or were, at least, waiting until it arrived) should ever run This deceptively simple, right? seems And working with just a bare , it actually pretty easy. While it might be an interface for creating asynchronous effects, return something immediately as soon as it’s called: a browser session-unique id. And if you’ve ever worked with , you know that you can simply use that id to cancel the entire operation before it completes: setTimeout is setTimeout does setTimeout But let’s note something very important here about scope: the exists in the scope. Which is to say: it exists in the present. The function, on the other hand, sort of exists in the future. What’s accessible in the present is still accessible in the future, and it’s thus to sneak the cancelation id into the callback function by using a mutable external variable, like this: cancelId outer x=>console.log(x) probably possible But hopefully you can see that doing this is sort of pointless: once the inner callback runs, it’s too late to cancel the timeout. It’s already run! Thus, the ideal time and place you to get access to any cancelation controls is really the exact same as the time you choose to execute a cancelable operation… and in the exact same scope. gets this right. setTimeout Now, the resources involved in making a call are relatively minimal, so saving CPU cycles isn’t as big a deal there as just stopping any side effects. But other asynchronous APIs be incredibly resource intensive. A network request for a big file. Or even an async file read operation. If our program or user decides that we don’t need to finish those operations, then optional cancelation offers a pretty huge possible performance win. setTimeout can Of all the asynchronous apis in the mix, there are two flavors: those that already use (i.e. return) Promises, and special snowflakes like . In the former group, we have things like the prospective new API, which is blocked mostly precisely because nobody is quite sure how to shoehorn cancelation into its Promise-based API. Promises, and their syntax sugar-y friends seemed like a very promising abstraction for clean, synchronously defined non-blocking async code. setTimeout fetch async/await But let’s take a quick look at the latter, supposedly old-school group, where we also have things like XMLHttpRequest and . With most of these interfaces, you first create an object that can dispatch an asynchronous action, define (or are given) some handlers and hooks (including ones for cancelation) on it, and then finally execute the action with all those callback-y things baked in: FileReader Critically, you fire off this execution step using that original control object: one that you have an in-scope reference to. That’s what we said we wanted! Great. Well… that’s exactly what you do NOT have in the case of Promises! You can, of course, wrap those special APIs inside of a Promise. But with those special snowflake APIs, or a WebWorker event channel or whatever: if you use them to create a new Promise, then those control references exist the scope of the Promise constructor function and are hooked up to, and to, the surface of the Promise. But as we’ve seen, nothing other than the resolved or rejected values will ever come back out again! You have no way back “into” the operation you’re running, and thus no real control over it. setTimeout inside only INNER Now, we play the same trick we played with : we can first define a variable in the outer scope and then just re-assign it inside the constructor, retaining access to it afterwards. This is basically what many (non-native) implementations of simple “cancelable” promises do. And in fact, this approach is sort of the genesis of the (now also retracted?) proposal. could setTimeout cancelation Token That’s a bit of a one-off, so most implementations tend to wrap up the entire construct in more layers so that they can return a unique cancelation method on the Promise object, as well as shims to make sure that the extension-y weirdness it gets carried through successive etc. chains. Or they model the token as a Promise itself, complete with an optional cancelation reason. .then().catch() But if that already sounds like a tremendous amount of headache and overhead for a fairly rare operation, you’re not wrong. And we haven’t even gotten into the incredibly confusing question of what happens to the inherited of derived Promises. state Derived and Dependent State As we said, Promises are in some sense first-class that you can pass around and attach additional handlers to at any time. But when an original effect is canceled, how do we model it down through a chain of stateful dependencies? We can’t let any success handlers get called (causing unwanted side-effects, based on values that we now don’t have and never will have). But what if the same Promise is used several times with different effects: do we model cancelation as an error (propagating it everywhere)? values .then() their I mean, cancelation really an error, nor should every affected function need to know how to catch it: they just need to not run at . So then… do we just never let any of these derived Promises resolve… ? Do we build some way to tell them that they’re never going to get a value OR an error (so that we at least can chain on some special cleanup method like to handle case)? isn’t all ever .finally() that There are lots of possible ways to answer those questions… and actually the problem! None are particularly natural or intuitive: you just have to sort of pick one and live with the downsides. Libraries like Bluebird handle the “multiple dependencies” problem, for instance, by . The original effect is canceled only if all the attached handlers cancel, throwing an error for any handlers that are attached after the cancelation occurs. that’s basically statefully keeping track of every consumer attached before an original promise is canceled It can work, but it’s still a pretty ugly, race-y, somewhat mind-bending system. But if you even slightly agree with me that Promise cancelation gets pretty gross, then let’s consider why this is all such a problem in the first place: because of . That is, Promises are not just descriptions of future events: they’re little state machines that we treat, and even sometimes think of, as , even though they’re really sort of value that you can map over. It of works, most of the time. And it’s deceptively exciting: we can store and pass references to values around even before they exist! statefulness itself values containers sort But it makes a lot sense when it leads us to talk about a value. Like a time-traveler that murders their own grandparents, a canceled value, by all rights, . In fact, we shouldn’t have a reference to it and thus we never could have/should have attached all these derived states to it in the first place! After all, if you travel back in time and prevent your grandparents from having kids, it’s not just that shouldn’t exist, your grandchildren shouldn’t either. less canceled should never have existed in the first place you And yet, that’s exactly the sort of bizarre construct we’re stuck with when using Promises as an abstraction for async operations! So, shoot. Are we going to be stuck with this mess when fetch and other Promise-based APIs roll out? Probably! But did it/does it have to be this way? Well, no. So remember how we bemoaned the fact that the callback in is not, per se, composable (or at least, once you’ve described a setTimeout, you can’t add anything further chained behavior to the callback)? Solving this with Promises was one possible solution. But it also introduced this “future value” abstraction that, as we’ve seen, is deeply troubling. setTimeout There is another possible solution though: a form of chain-ability not by using linked states, but instead by implementing “lazy” operations. We’re talking of course, about the functional type known variously as a or a . Future Task There are lots of great / libraries out there, but because the core concept is so fundamentally simple, let’s code up a right here and now: Task Future Task Yep, that’s the core of the functional alternative to Promises, all in just a couple of code. And yet, its constructor usage is astonishingly pretty similar to Promises! characters Now, that won’t actually anything, of course: we have to run by passing it two handlers that match the ones we defined in the constructor. do .fork() Note that the cancelation interface is exposed without any extra work here: calling . very naturally the id token that we can then call on if we ever want to: right out of the box fork returns setTimeout clearTimeout Pretty neat, no? If we wanted to be more generic, we could just require that all constructors return a generic cancelation function directly, meaning we just define and return it from the constructor, standardizing cancelation usage regardless of the specific details of how some browser-level async operation can be canceled: Task In any case, the point here is that are not fancy “future values” in the same way that Promises are: they’re just descriptions of future . And as such, they have another key feature missing from Promises: functional purity. That is, defining a has no side-effects in the way that defining a Promise does. Every constructor returns the same thing: a Task holding that same constructor, still unexecuted. Nor does a Task “keep track” of whether “it” is “pending, resolved, or rejected.” Tasks are never any of those things because they’re just . Tasks computations Task Task descriptions And thus, as such, there’s never any such thing as a “canceled” that we have to worry about in the first place. You can’t cancel a , by definition, because it’s an operation that hasn’t run yet! Once you run it by calling fork, you’re actually no longer really dealing with a any longer (the defined itself is immutable/reusable/extendable): all that’s left is the operation, its eventual side effects, and whatever controls you’ve left yourself to alter it in the meantime. Functional types tend to “get out of your way” like this once they’ve served their effect-ful purpose. Task Task do Task Task What’s additionally great about all all this, as I tried to celebrate , the method isn’t some special magic, and the arguments to a constructor aren’t some weird internal interface the way they are with Promises. is literally just a portal right back into the constructor definition itself: the / callbacks you attach using are literally the / arguments passed to the constructor! in my original article on Tasks .fork reject/resolve Task’s fork onFailure onSuccess .fork() reject resolve Task’s But what about composing subsequent operations onto the constructor’s result? Easy: Adding operations onto the rejected or resolved branch of the constructor likewise has no side-effects: it simply describes a referentially transparent transformation from any one value to another. We didn’t even have to do anything special to allow the original id to still come back out at the end! Task setTimeout Pretty much everything you can do with a Promise you can also do with a , (though most of the other operations, like the monadic transform from a value into a new Task that we’re used to handling implicitly, take more code and have to handle more edge cases in their internal implementation than is worth going into here). But critically, the cancelation interface can be exposed right where we need it. All we had to give up was this weird concept of first-order “future” values that was probably more trouble than it was worth in the first place. Task and actually much more .then() In any case, whatever cancelable interface we ultimately end up with for Promise-based apis like will probably work out ok, and let us basically do what we need to do… but there’s no getting around how problematic it’s likely to be to work with and reason about. fetch Personally, knowing that an alternative pattern exists (and that I’ll just be able to wrap a around it all and escape having to deal with it), is quite a relief! Task And of course, there’s ! always Observables Addendum: zalgo considerations One upshot of the straightforward nature of Tasks is that the that between sync/async into Promises as they stand. There’s no mandatory / requirement making sure that effects always “run” asynchronously to avoid confusion about the order of things. Our whole purpose here seemed to be figuring out how to do asynchronous operations, but . They do this, again, by separating the of an effect and any transformations from the actual moment that it’s executed and told how to of itself. “zalgo” danger required ES6 Promises code in a forced distinction doesn’t exist with our Tasks setImmediate nextTick Task Tasks eliminate the distinction entirely description dispose Because of this, can be very naturally used to model either synchronous callbacks (functional dependencies) asynchronous ones (time-based dependencies). That’s because it’s the final operation that explicitly controls the execution timing (execute this side-effect-causing operation starting right…. !). Tasks or .fork now work just as seamlessly across sync or async effects in the same way that our original callback method did. If you didn’t even care about cancelation you could have just returned and used a synchronous value directly from the constructor function, after all. Tasks But this does mean that should be careful with any value-generating side-effect that could even run either synchronously or asynchronously. Just be wary about side-effects (like a network request with synchronous caching or memoization) that could do either, and if so, be either extremely careful of when you execute them (since any other synchronous code after them could run before OR after they run without a nextTick guard) or just force a onto the effect yourself. you possibly setImmediate You have more power here… but also more responsibility. The “zalgo” problem occurs when side-effects aren’t always sequenced properly (and so could run in an unexpected order). This isn’t a bad thing really: once you have more unified control over the ordering of ALL side-effects, you can chaining them all in a specific control flow instead of just writing them out as a bunch of lines that may or may not run in “happenstance” order. explicitly is how hackers start their afternoons. We’re a part of the family. We are now and happy to opportunities. Hacker Noon @AMI accepting submissions discuss advertising &sponsorship To learn more, , , or simply, read our about page like/message us on Facebook tweet/DM @HackerNoon. If you enjoyed this story, we recommend reading our and . Until next time, don’t take the realities of the world for granted! latest tech stories trending tech stories