JS / How to manage complex async flows in your app

Or how i learned to deal with chaos

Vladislav Bogomaz
Vladislav Bogomaz
5 min readOct 6, 2019

--

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.

But what if you have many nested async functions? What if some of them are logically cancellable, while other ones— not? What if you need to provide to user correct feedback in UI on execution process, not only on result?

Whenever more questions appear, you become more focused on how to do it, not on what you are doing.

Solution patterns

Execution process

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

Ok, it was easy. Let’s think about exceptions.

Now your function is littered more with code that is not related directly to what you are doing inside the function. You may want to shorten previous example with try/catch/finally statement around whole code, but it does not help to deal with different behaviour needed inside catch blocks. You have to keep in mind all that logic and make changes carefully.

Is it possible? Of course, yes. Does it looks good? Well, i think — no.

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:

Pattern is clear. But your function is not as before. What if you have many async actions to call step-by-step? Easy, you will put more and more if statements inside your function.

Same again. Is it possible? Yes. Does it looks good? I’m not sure.

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:

You can see the difference — clean and readable code. No more focus on how, but only on what.

So, where are status changes, cancellation, nesting, etc are handled? How to make everything work?

Meet Interruptible-Tasks! That tiny library aimed to solve all mentioned problems and to provide you awesome experience on building complex flows.

Are you building new project? Just dive into tutorial in README.MD and explore how to adopt your async functions to be used as generators. There are also some examples to start from.

Do you have some code already? No worries, there is no need to write anything from scratch. Minimal changes will be required. Please find more advanced examples by exploring tests of Interruptible-Tasks. Do not hesitate to come back to README.MD for API reference.

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

Here we can see connect function, which could wire updates from Interruptible-Tasks to any state used in your app (redux, mobx, vuex, vue-stash, hyperapp, your custom state like in example). It become trivial to notify user on inner running tasks.

But what about nested functions? Just convert them to Tasks and wire with same connect function. Done!

Cancellation

Whenever you needed, just call .cancel() function and you’re done. Your Task’s Promise will be rejected as soon as next yield statement inside Task is reached. If passed, connect will be called automatically.

Nested cancellation works seamlessly if you have yielded nested Task in your function. Cancellation event will bubble down to all Tasks running. All Tasks statuses will be updated correctly and updates will be passed to corresponding connect functions you have provided.

Interruption

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

If you made your Task interruptible (interruptible:true ) you have ability to run new Task and to have old one interrupted automatically.

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.

Anyway, you have to make a reasonable choice and i believe you can.

I’m really proud to present library that I’m using in my own projects to make code clean and modular. It helps to keep complex architecture solid and extendable.

If you have any questions or issues, or found an error, please feel free to reach me on GitHub project page or on Twitter.

--

--