JS / How to manage complex async flows in your app

Or how i learned to deal with chaos

Vladislav Bogomaz
Oct 6, 2019 · 5 min read
Image for post
Image for post

Problem

Almost every app needs to have granular control over its asynchronous flows. When it comes to long-running functions, you may want to be able to cancel them and to precisely know whats going on. It looks simple to put some code around your function to handle that case once.

Solution patterns

Execution process

It is trivial to add some code to your function to do it. Let’s dive into example.

async function longRunningFunc() {
setStatus('pending');
...
setStatus('stopped');
return result;
}
async function longRunningFunc() {
setStatus('pending');
...
try {
data = await anotherFunc();
} catch (error) {
setStatus('stopped');
return error;
}
...
try {
data = await anotherNonCriticalFunc();
} catch (error) {
...
}
...
setStatus('stopped');
return result;
}

Execution process of nested functions

In previous section we have a good example of how to do it once. But how many functions do you have in your app? Multiply all the trash code by that amount and try do not forget all the relations after you are back from your vacation. I will provide way to solve that problem below.

Cancellation

Let’s imagine situation. User types something in text input and you need to fetch suggestions. Ok, we’re already debounced our handler. But why do we need to continue already ran fetch if user has continued to type? We can do it by following code:

let makeCancel = false;
async function makeFetch(input) {
makeCancel = false;
setStatus('pending');
...
try {
data = await fetchSuggestions(input);
} catch (error) {
setStatus('stopped');
throw error;
}
if (makeCancel) {
return;
}
...
putInState(data);
...
setStatus('stopped');
return result;
}

Nested cancellation

At first glance it looks not so easy. And it is not easy. But fortunately JavaScript has great instruments to deal with it. Solution presented below uses generators and promises under the hood to provide you excellent experience and to solve all mentioned problems.

Getting all together

Let’s dive right in example how your code could look like. Consider the example on the same function as above:

function* makeFetch(input) {
...
try {
data = yield fetchSuggestions(input);
} catch (error) {
throw error;
}
...
putInState(data);
...
yield result;
}

Examples

Let’s solve problems mentioned in the Problem section one by one. To focus on valuable things I will not describe whole API of the library, but it should be easy to understand. If not — please find detailed description in README.

Execution process

import { createTask, taskStatuses } from 'interruptible-tasks';const taskName = 'connectedTask';
const stateImitation = new Map();
const connect = (name, status) => {
stateImitation.set(name, status);
};
const task = createTask(
function*() {
yield new Promise(resolve => setTimeout(resolve, 10));
},
{ interruptible: false, cancelable: false, name: taskName },
connect
);
let runPromise = task.run();
console.log(stateImitation.get(taskName) === taskStatuses.pending); // true
await runPromise;
console.log(stateImitation.get(taskName) === taskStatuses.stopped); // true

Cancellation

import { createTask } from 'interruptible-tasks';const task = createTask(
function*() {
yield new Promise(resolve => setTimeout(resolve, 10));
yield data;
},
{ interruptible: false, cancelable: true, name: 'demoTask' }
);
const runPromise = task.run();
console.log(task.cancel()); // true
await runPromise.catch(error => console.error); // TaskHasBeenCancelledError('Task demoTask has been cancelled')

Interruption

One new benefit you have now is Interruption pattern. Why to call .cancel() manually if you only need to start your task again?

import { createTask } from 'interruptible-tasks';const task = createTask(
function*(data) {
yield new Promise(resolve => setTimeout(resolve, 10));
yield data;
},
{ interruptible: true, cancelable: false, name: 'demoTask' }
);
task.run('not ok').catch(e => console.error); // after 10ms: TaskHasBeenInterruptedError('Task demoTask has been interrupted')
await task.run('ok'); // after 10ms: 'ok'

Conclusion

Maybe you have a question like: Why not to use redux-saga or similar? My answer is: Why you have to? Maybe for particular project it is enough, or it is best solution indeed, but there also could be reasons not do so. You may not want to use such libraries for such small task; Maybe such libraries do not provide everything you need; Maybe you don’t want to be tied too much to any ecosystem like react+redux+redux-saga.

Vladislav Bogomaz

I work in Amazon and I like to write about tech

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store