Javascript Async programming: Pitfalls and how to avoid them

Javascript has evolved immensely in the past decade. A new culture started rising where developers are contributing to the community, trusting many external packages and being MEAN. The reason behind this is everyone was getting too tired of writing different code on the client and server, designing transportation protocols, serializing/deserializing all the data and not being able to use this code anywhere else in their own project. Thanks to Node.js, that world is far behind now. We can write backend and frontend code in Javascript and the V8 engine is so fast, we don’t even need to sacrifice performance anymore. And the biggest game changer was Async programming. Thanks to the async nature of javascript, all the code can be executed in one Thread. What does this means for you;

  1. No more multi-threaded design chaos
  2. No more locks, mutexes, semaphores
  3. No more dead locks!
  4. No more crazy amount of context switches which are known to be super costly
  5. No more threads being blocked on IO operation (Personally, this is the biggest win. Your threads doesn’t need to spin empty until a data is being prepared somewhere else)

Evolution of async programming;

Everything started with a Callback

Callbacks were a powerful way of programming as it’s completely async and doesn’t block the threads. But there are multiple issues with them;

  1. Really hard error handling. Since all calls to the callback methods are executed on a different context, you can’t simply throw an Error. You have to create an error and pass it all the way to the callback methods and write really complicated handling.
  2. They’re so hard to debug. Since each call back is called on a different context, you can’t step over methods. You need to set breakpoints into each method and run your code and hope to hit your breakpoint
  3. Impossible to follow stack traces. If your application crashes for some reason, you’ll be amazed to see the stack trace. Since your main thread that called, updateFile() already exited, the rest of the code is run on different contexts as methods finishes processing data and in case of an exception being thrown, your call stack will be impossible to follow
  4. Ugly code. As you see for such simple operation how ugly the code has become. You can imagine how worse it can get from here easily.

I PROMISE, it’ll get better

The solution to the code above was introducing promises. They’re really smart way to simplfy code by turning a callback into an Object called Promise that resolves upon completion.

Promises made async programming much simpler. Now you can just return promises and keep piping them into each other to do everything step by step. To see how our previous example looks with them;

Pitfall #1: Don’t do then().catch().then().catch()

A lot of developers assume the code after the first catch() block will not be run. That’s not the case at all. Equavalent of this code is;

Pitfall #2: Don’t trust catch() if you’re not fully on Promises

Async/Await Programming

As a solution to all our problems listed above(And many more), async/ await was introduced. Async/ Await programming has been around for a while now thanks to Microsoft.Net but it was only working on .Net languages like C#. In the recent years, Javascript decided to follow the pattern as well which makes programming a breeze. The idea behind async/await is too simple to be true;

This is all a mind trick invented by some really smart people. What’s happening underneaths the code above is mind blowingly simple;

  1. The result of the functions defined by async is turned into a Promise.
  2. Where ever there’s an await, it’s turned into a state machine which’s paused until the promise being called is resolved.

Benefit of Async/Await

  1. Easy error handling. Since the code looks like a super simple sync programming, it becomes really easy to assign values into variables and follow what’s going on.
  2. Super easy debugging. Debugging becomes a breeze as you can iterate line by line and don’t need to set many break points
  3. Clean stack traces. Now if your program were to throw an Error, it’ll have a cleaner call stack showing exactly what happened in which method
  4. Throw/Catch is usable one more time! Since all these code will be run in same context, you can safely throw and catch errors without thinking what context your code is running under.

Pitfall #3: Avoid mixing Async / Await and Callbacks

At the end of the days, you might think all of these concepts resolves into same thing. But because of the problems with the Context I’ve described earlier, it becomes really dangerous to mix and match these programming styles. For example if you have a code similar to Pitfall#2, you might assume try{} catch (){} block will catch the errors thrown inside the callback method but it won’t. As I mentioned, call back method will run in a different context and you’ll be losing the context.

Go all the way Async / Await and turn everything into Promises


Leave a Reply

Your email address will not be published. Required fields are marked *