xBlog

BLOG

Design cancellable asynchronous callbacks

Design cancellable asynchronous callbacks
Mohamed EL AYADI

Mohamed EL AYADI

20 September, 2021 Β· 5min πŸ“–

Without a doubt, you've probably already worked with callbacks in your coding journey, which means that you know how much of a nightmare and how unpredictable asynchronous callbacks can be. In this article, we will explore by example the problems we face, and some of the easiest solutions that you may implement.

First, let's evaluate this code

function search(criteria) { // search by this criteria
  doSomething( // do something with the criteria
    criteria,
    function onSuccess(data) { // and later, call this function with the result
      updateTheUI(data);
    }
  );
}

If you think that this code is working and good, you have failed your first test!

Let's get to the point, what happens if the search function is called concurrently ? You do not control when doSomething will call your success callback that updates your UI... What if a subsequent call finishes its invocation before the former ?

Heavy breathing cat GIF

When the former answers (or resolves), your success callback will update UI with the earliest search criteria's result, and thus, your UI will be out of sync with what the user's action is supposed to output.

If you still don't have a clear idea of the problem, I invite you to take a close look at this code sandbox. Open the sandbox, open the dev tools, and then write 123 in the search input. You will notice instantly 3 requests in the network tab of your dev tools:

search results

But later, after they finish running and callbacks are called, you will have this UI (there is no uglier than this, I know!) with a different random color*:

wrong result

Now, you don't need to have a compiler in your head to know that we screwed up and we are showing the results of the first-ever search: when we typed 1.

You may be thinking of debounce, throttle, takeLatest, takeLeading, and a lot of other cheap solutions that you may have encountered in your journey. But do they solve the problem? No! by no chance:

  • debounce and throttle: It is easy to break them, just wait until you are sure the debounce/throttle delay is passed, then search again, and then you've come back to the starting point!
  • takeLatest and takeLeading on the other hand seem to solve the problem, but they don't follow if you need to invoke a cleanup function of the aborted operation. Here is an exercise to do: replace takeLatest with takeEvery and see how your code behaves.

Don't worry, we are not being pessimistic, we just want to solve the problem properly, once and for all. Even though the previously cited techniques may partially solve the problem, it is better if your mind knows other (proper) solutions to the same problem.

We will now explore two easy solutions**, one using closures, and the other using callbacks (yes callbacks to solve callbacks).

1. Prevent a callback from being executed

The first solution consists of preventing old callbacks from being executed, like this:

const index = 0;
function executeSideEffectWithCallback(value) {
  index++;
  const callIndex = index;
  function callback(data) {
    if (callIndex === index) {
      printData(data);
    } else {
      console.log('prevented an old callback!');
    }
  }
  invokeSideEffect(value, callback);
}

The trick is that each time you call your function, you increment an integer variable and you capture its value. Later, in the callback function, you compare the captured value (via closure) with the original value. If there was another call between our initial call and the callback, callIndex and index won't have the same value, and then we can decide not to do a portion of the work.

Of course, you will need to adapt this code to your needs, for example, using react and during render, declaring an index=0 will always give you zero and the solution won't work, you will need sort of a mutable object that lives with the component (a ref). Let's solve the previous bug we had using this technique in this sandbox. Again, open the dev tools and see what's happening, understand the problem along with the solution.

Note that the fetch requests aren't aborted, they finish their work normally and then call your callback. Once your callback is invoked, it checks whether it is about the last registered call, if yes it is invoked.

2. Register a cleanup callback

This technique lets you define how to dispose of whatever your side effect executor is doing: Cancel the fetch, clear a timeout/interval, abort a generator (or even more cool things).

Although, this requires you to pass the abort registerer to your side effect executor. Here is an example:

function delayedData(data, onSuccess, onAbort) {
  const timeoutId = setTimeout(() => {
    onSuccess(data);
  }, delay);
  onAbort(() => clearTimeout(timeoutId));
}

//...

let currentAbort = null;
function caller() {
  invokeIfPresent(currentAbort);
  currentAbort = null;
  delayedData(
    search,
    function onSuccess(data) { printData(data); },
    function registerAbort(cb) {
      currentAbort = cb;
    }
}

Designing your code this way ensures that for every side effect you execute, you support to clean it and a way to register that cleanup callback*** (let's call it two way abort-binding!). This will ensure that you will always have one living callback at a time.

Let's go back to our initial example and solve it using this technique, see this codesandbox. Notice that now, you are able to see this in your network tab:

solution 2

So the basic idea is whenever you find yourself executing an asynchronous call with a callback, ask yourself the following questions before finishing your task:

  • Am I executing something abortable ?
  • Do I need to abort the previous call ?
  • Can I have multiple cleanup callbacks ?

Depending on the answers, re-check your design and make sure you are not having concurrency problems.

Conclusion

By now, you should be aware of the asynchronous callbacks concurrency problem and you also should be able to solve it using various techniques. Now the discussion brings us to the side effect executor and how to abort/interrupt it. Let's leave this discussion to another post which will talk about it exclusively.

*: A random color as body's background color was set so you notice each UI change in a more attractive way.

**: You may just do an effect with cleanup on the search criteria in our example, but it won't solve all your everyday problems, or you will end up with a lot of code duplication and useEffects for any side effect.

***: The cleanups you register may just toggle the value of a boolean that prevents the callback from being executed.

See you next time! Don't hesitate to reach out to me or any member of the team in case you have any questions.

Mohamed EL AYADI

Mohamed EL AYADI

Hi, I'm Mohamed and I am a software engineer. I like to build efficient and secure apps. I am a java and javascript developer.

Tags:

signature

Mohamed EL AYADI has no other posts

Aloha from xHub team πŸ€™

We are glad you are here. Sharing is at the heart of our core beliefs as we like to spread the coding culture into the developer community, our blog will be your next IT info best friend, you will be finding logs about the latest IT trends tips and tricks, and more

Never miss a thing Subscribe for more content!

πŸ’Ό Offices

We’re remote friendly, with office locations around the world:
🌍 Casablanca, Agadir, Valencia, Quebec

πŸ“ž Contact Us:

🀳🏻 Follow us:

Β© XHUB. All rights reserved.

Made with πŸ’œ by xHub

Terms of Service