All four functions called below in update
return promises.
async function update() { var urls = await getCdnUrls(); var metadata = await fetchMetaData(urls); var content = await fetchContent(metadata); await render(content); return; }
What if we want to abort the sequence from outside, at any given time?
For example, while fetchMetaData
is being executed, we realize we no longer need to render the component and we want to cancel the remaining operations (fetchContent
and render
). Is there a way to abort/cancel these operations from outside the update
function?
We could check against a condition after each await
, but that seems like an inelegant solution, and even then we will have to wait for the current operation to finish.
Async/await has a synchronous behavior, so yes it will block the current respective execution flow until it is finished. no, it won't block the thread.
await only blocks the execution of the next lines of code in an async function and doesn't affect the promise execution.
In my view, unless a library or legacy codebase forces you to use then/catch , the better choice for readability and maintainability is async/await . To demonstrate that, we'll use both syntaxes to solve the same problem.
The await expression causes async function execution to pause until a Promise is settled (that is, fulfilled or rejected), and to resume execution of the async function after fulfillment.
The standard way to do this now is through AbortSignals
async function update({ signal } = {}) { // pass these to methods to cancel them internally in turn // this is implemented throughout Node.js and most of the web platform try { var urls = await getCdnUrls({ signal }); var metadata = await fetchMetaData(urls); var content = await fetchContent(metadata); await render(content); } catch (e) { if(e.name !== 'AbortError') throw e; } return; } // usage const ac = new AbortController(); update({ signal: ac.signal }); ac.abort(); // cancel the update
OLD 2016 content below, beware dragons
I just gave a talk about this - this is a lovely topic but sadly you're not really going to like the solutions I'm going to propose as they're gateway-solutions.
Getting cancellation "just right" is actually very hard. People have been working on just that for a while and it was decided not to block async functions on it.
There are two proposals attempting to solve this in ECMAScript core:
catch cancel (e) {
syntax and throw.cancel
syntax which aims to address this issue.Both proposals changed substantially over the last week so I wouldn't count on either to arrive in the next year or so. The proposals are somewhat complimentary and are not at odds.
Cancellation tokens are easy to implement. Sadly the sort of cancellation you'd really want (aka "third state cancellation where cancellation is not an exception) is impossible with async functions at the moment since you don't control how they're run. You can do two things:
Well, a token signals cancellation:
class Token { constructor(fn) { this.isCancellationRequested = false; this.onCancelled = []; // actions to execute when cancelled this.onCancelled.push(() => this.isCancellationRequested = true); // expose a promise to the outside this.promise = new Promise(resolve => this.onCancelled.push(resolve)); // let the user add handlers fn(f => this.onCancelled.push(f)); } cancel() { this.onCancelled.forEach(x => x); } }
This would let you do something like:
async function update(token) { if(token.isCancellationRequested) return; var urls = await getCdnUrls(); if(token.isCancellationRequested) return; var metadata = await fetchMetaData(urls); if(token.isCancellationRequested) return; var content = await fetchContent(metadata); if(token.isCancellationRequested) return; await render(content); return; } var token = new Token(); // don't ned any special handling here update(token); // ... if(updateNotNeeded) token.cancel(); // will abort asynchronous actions
Which is a really ugly way that would work, optimally you'd want async functions to be aware of this but they're not (yet).
Optimally, all your interim functions would be aware and would throw
on cancellation (again, only because we can't have third-state) which would look like:
async function update(token) { var urls = await getCdnUrls(token); var metadata = await fetchMetaData(urls, token); var content = await fetchContent(metadata, token); await render(content, token); return; }
Since each of our functions are cancellation aware, they can perform actual logical cancellation - getCdnUrls
can abort the request and throw, fetchMetaData
can abort the underlying request and throw and so on.
Here is how one might write getCdnUrl
(note the singular) using the XMLHttpRequest
API in browsers:
function getCdnUrl(url, token) { var xhr = new XMLHttpRequest(); xhr.open("GET", url); var p = new Promise((resolve, reject) => { xhr.onload = () => resolve(xhr); xhr.onerror = e => reject(new Error(e)); token.promise.then(x => { try { xhr.abort(); } catch(e) {}; // ignore abort errors reject(new Error("cancelled")); }); }); xhr.send(); return p; }
This is as close as we can get with async functions without coroutines. It's not very pretty but it's certainly usable.
Note that you'd want to avoid cancellations being treated as exceptions. This means that if your functions throw
on cancellation you need to filter those errors on the global error handlers process.on("unhandledRejection", e => ...
and such.
You can get what you want using Typescript + Bluebird + cancelable-awaiter.
Now that all evidence point to cancellation tokens not making it to ECMAScript, I think the best solution for cancellations is the bluebird implementation mentioned by @BenjaminGruenbaum, however, I find the usage of co-routines and generators a bit clumsy and uneasy on the eyes.
Since I'm using Typescript, which now support async/await syntax for es5 and es3 targets, I've created a simple module which replaces the default __awaiter
helper with one that supports bluebird cancellations: https://www.npmjs.com/package/cancelable-awaiter
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With