Instantly share code, notes, and snippets.

@jakearchibald /1.js Secret
Last active Mar 1, 2017

Embed
What would you like to do?
async function performFetch() {
startSpinner();
try {
const response = await fetch(url);
displayData(await response.json());
}
catch(err) {
if (err.name != 'AbortError') displayError();
}
stopSpinner();
}
@staltz

This comment has been minimized.

Show comment
Hide comment
@staltz

staltz Mar 1, 2017

For fetch, there isn't much that can be done beyond passing e.g. a FetchController to fetch(). The other alternative, of returning a controller as the output of fetch would lead to problems when users write fetch(url).then(....).

So if you're interested immediately into how to solve fetch cancellation, I'd say you're on the right track with passing an additional argument to fetch(), because of backward compatibility with its Promise-based API.


That said, I want to talk about the broader topic of async Browser APIs.

There has to be a separation between async task definition and async task execution. https://github.com/rpominov/fun-task explains the issue properly, but it's also something known in RxJS.

Task definitions are composed together, but no execution has taken place yet. You can spawn an execution from the definition, and from that point onwards you usually have some object that represents the ongoing execution. For Observables, this is subscription. It's only job is to represent the ongoing execution of some task. It has a method unsubscribe attached to it. In Fun-task, the return of run is the cancel fn, so it forgoes the need for an object, but the idea is the same.

So, in summary:

  • They separate definition from execution
  • Abort only makes sense for execution, not for definition

That explains why Promise-based APIs have problems with cancellation because:

  • Promises conflate definition with execution
  • Because of the above, you have to conflate abort with definition
  • You cannot compose (.then()) Promises-as-tasks while avoiding their execution

What can be done for async browser APIs with what the platform offers today? Suggestion: allow representing definition and composition separately from execution. Async task definition can happen in the form of an object (e.g. URL string or Request), then the executor of that task can be a function like const execution = run(definition), and then you can "control" the execution through methods attached, like execution.cancel(). Or even const cancel = run(definition)

Notice how callbacks offer us a way of defining tasks and even composing tasks without executing anything yet. This is callback hellp, but at least it only does async task definition and composition:

function getMyData(x){
    getMoreData(x, function(y){
        getSomeMoreData(y, function(z){ 
            ...
        });
    });
}

Because getMyData() is never called, this never executes. And we were able to compose with other tasks like getMoreData and getSomeMoreData.

Promises don't have this property. As soon as you construct a Promise, it is on its way towards execution already (in the next tick, though).

Of course callbacks don't yet have cancellation const cancel = run(getMyData), but they aren't far from providing that either. All you need to do is have a contract in place where each callback should synchronously return a cancel function. So then, when composing the callbacks, you can call cancel of each one. Take this idea further, to provide a better API, and soon you'll have reinvented RxJS.

So I'll I'm saying is even though callbacks are prone to "callback hell", they are a better lower-level abstraction than Promises for async because they provide this ability to separate definition/composition from execution. And eventually, if/when Observables are in the platform, we could easily wrap callback-based APIs into a better API. This is how RxJS uses callback-based XHR for its Ajax helpers, because XHR at least provides the notion of run (xhr.send) as well as cancel (xhr.abort).

As for fetch, gotta go with what is in whatwg/fetch#447.

staltz commented Mar 1, 2017

For fetch, there isn't much that can be done beyond passing e.g. a FetchController to fetch(). The other alternative, of returning a controller as the output of fetch would lead to problems when users write fetch(url).then(....).

So if you're interested immediately into how to solve fetch cancellation, I'd say you're on the right track with passing an additional argument to fetch(), because of backward compatibility with its Promise-based API.


That said, I want to talk about the broader topic of async Browser APIs.

There has to be a separation between async task definition and async task execution. https://github.com/rpominov/fun-task explains the issue properly, but it's also something known in RxJS.

Task definitions are composed together, but no execution has taken place yet. You can spawn an execution from the definition, and from that point onwards you usually have some object that represents the ongoing execution. For Observables, this is subscription. It's only job is to represent the ongoing execution of some task. It has a method unsubscribe attached to it. In Fun-task, the return of run is the cancel fn, so it forgoes the need for an object, but the idea is the same.

So, in summary:

  • They separate definition from execution
  • Abort only makes sense for execution, not for definition

That explains why Promise-based APIs have problems with cancellation because:

  • Promises conflate definition with execution
  • Because of the above, you have to conflate abort with definition
  • You cannot compose (.then()) Promises-as-tasks while avoiding their execution

What can be done for async browser APIs with what the platform offers today? Suggestion: allow representing definition and composition separately from execution. Async task definition can happen in the form of an object (e.g. URL string or Request), then the executor of that task can be a function like const execution = run(definition), and then you can "control" the execution through methods attached, like execution.cancel(). Or even const cancel = run(definition)

Notice how callbacks offer us a way of defining tasks and even composing tasks without executing anything yet. This is callback hellp, but at least it only does async task definition and composition:

function getMyData(x){
    getMoreData(x, function(y){
        getSomeMoreData(y, function(z){ 
            ...
        });
    });
}

Because getMyData() is never called, this never executes. And we were able to compose with other tasks like getMoreData and getSomeMoreData.

Promises don't have this property. As soon as you construct a Promise, it is on its way towards execution already (in the next tick, though).

Of course callbacks don't yet have cancellation const cancel = run(getMyData), but they aren't far from providing that either. All you need to do is have a contract in place where each callback should synchronously return a cancel function. So then, when composing the callbacks, you can call cancel of each one. Take this idea further, to provide a better API, and soon you'll have reinvented RxJS.

So I'll I'm saying is even though callbacks are prone to "callback hell", they are a better lower-level abstraction than Promises for async because they provide this ability to separate definition/composition from execution. And eventually, if/when Observables are in the platform, we could easily wrap callback-based APIs into a better API. This is how RxJS uses callback-based XHR for its Ajax helpers, because XHR at least provides the notion of run (xhr.send) as well as cancel (xhr.abort).

As for fetch, gotta go with what is in whatwg/fetch#447.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment