Coding Asynchronously in JavaScript (and other languages).
There is nothing new here — coding asynchronously has always been a staple of high-throughput server-side software, not to mention every application ever written with a graphical UI. There are multiple methods to code asynchronously, and many religions exist amongst these methods.
Fundamentally, asynchronous code takes the what might otherwise have been a single-flow of execution with a lot of waiting around, and schedules parts for some future time. The primary reason for asynchronous code is to make better use of your computing resources — things like network and disk IO are glacially slow compared to the ability of the CPU to crunch and process data. In the graphical UI world, your primary motivation is to never have your UI paused because your main thread is waiting on network or disk IO. You can either force your CPU to twiddle its pins (thumbs) or you can let the CPU do other things while you wait — it is ultimately your choice, and I hope to arm you with a better understanding here.
Multiple Processes or Multiple Threads
Once we obtained good preemptive multitasking operating systems, it became easy for us to get more done by running blocking code in a separate thread or process. When it blocked, we knew that the operating system would remove the process from the CPU and schedule other, waiting processes. While this is more commonly considered concurrent development, it fundamentally operates similarly to the asynchronous, event driven development we will discuss.
Another use of processes and threads is to take a piece of work that we know will consume a large amount time on the CPU, and intentionally spawn a worker process or thread to take care of that work. This allows the main thread to continue with other work — including, possibly, scheduling more work on more secondary processes or threads.
There is a downside to threads and processes — they have a fixed, non-zero cost associated with them. In our modern operating systems the overhead has been greatly minimized, but there is still memory overhead for each and context switching overhead if you are cycling between many different process contexts.
Event Driven Development
Asynchronous development makes use of an event loop running on a single thread or process. For IO heavy applications (network or disk), your code is idle the vast majority of time and blocking a single thread or process is a waste of precious resources. Event Driven Development works by having an event loop as the central processing paradigm in your process. You start an asynchronous operation by including a callback which will do the next stage of work once the IO completes.
Let us momentarily return to the multi-thread/multi-process paradigm from the last section: The thread or process does some work, schedules some IO, and then the kernel puts it to sleep until that IO completes — when that completion event occurs in the kernel, the process is scheduled to run once more. This is “essentially” event-based asynchronous development by relying on the ultimate event loop of the kernel to schedule us for execution. It is functionally a waste of resources, because at scale fewer processes are going to utilize less CPU and memory resources — bigger bang for the buck is always a good thing!
First let us understand the event loop: When you use an event loop, you give control of the thread over to the event loop and you never expect to have control back until the program terminates. This event loop is charged with executing code at specific points in time which you, the developer control. This typically starts with a starter function to sets things in motion like displaying the first window or setting up a listening TCP socket and registering a callback to use when a new connection is detected.
Registered callbacks tied to specific events are the handlers the event loop executes when those event occurs (eg. a click, or an inbound connection attempt). Callbacks for UI events, network and file system IO events, and timers can be scheduled for execution on an event loop.
If you think about all of the development you have ever performed, you will find that most of it comes down to waiting on input (from the user, from the disk, and from the network) and waiting on timers (scheduled tasks, timeout tasks). Once you realize that, you will see that the event development model is well suited to most of your work.
Event loops tend to be very trivial (even if their actual implementation under the hood are complex). First-In-First-Out (FIFO) is the easiest paradigm — all events schedule for execution on the event loop are executed in the order received. Typically, as an application runs, event handlers will be registered and unregistered as things happen; for example, a TCP based server process will accept a new connection and register a handler to execute when data is ready to read on that new connection and when the connection is closed the handler is removed from the event loop.
If you are working in an application with a UI, you are already doing event based development.
There are a couple of coding paradigms which enable this style of event loop processing. These include callbacks, promises, and async/await. We are going to take a look at each them in JavaScript below, but these concepts apply to all of the languages which have promises and async/await. Thankfully the greater language ecosystem is keeping these concepts relatively closely related even across languages.
Asynchronous Methodology 1 — Callback Hell
Registering callbacks is the earliest method of working with an event loop. In some cases you simply cannot get away from registering callbacks; for example in a UI application you just have to register that button callback. In those instances it is not callback hell. I will show you callback hell in a few.
Let’s look at a simple example:
Here’s a breakdown of that code:
- When the function x is executed, it calls the
fs.writeFile()
which registers your callback and initiates the file system work. - When
fs.writeFile()
returns,x() returning
is logged to the console, and the function x returns - At some point in the future,
The file is now written!
will appear on the console indicating completion of the file system write.
This is asynchronous programming. In the intervening time while the slow IO of fs.writeFile()
was pending completion, the calling function was able to continue doing work and eventually returning to allow the event loop to process other events.
This callback scheme is not too terrible. It only has one level of nesting going on. Here is what I call Callback Hell and why I am not a fan of this older method of doing asynchronous development:
Here we are attempting to open, write to, and close a file all completely asynchronously. Each of these operations can block the processor for an eon in terms of processor time. The Callback Hell is this deep nesting that makes software developer’s eyes bleed and their brain hurt in following and tracking many of such blocks of code. It is pure hell.
Asynchronous Methodology 2 — Promises
Many languages now support a concept of a promise. This is nothing more than a simple wrapper around an asynchronous call where you no longer supply the callback to the asynchronous call but to the wrapper. The promise wrapper ensures that whatever you register will get executed at the proper time in response to the completion of the promise.
Here you see promise chaining in action. Promises hide and mask a lot of the nesting that you were seeing in the callback methodology, and that is a good think. The first .then(…)
receives the result of the promise, and all subsequent ones receive the result returned by the previous one in the chain. This improves upon the callback hell because it helps to minimize our indentation — we are not in danger of boundless indentation due to nested asynchronous operations.
Asynchronous Methodology 3 — Async/Await Heaven
In several languages, we now have a new way of coding asynchronously to tell our language-of-choice that we expect to wait for something to finish before continuing on. It allows us to write the asynchronous code as if it were a series of synchronous operations — this is only an appearance thing. Let’s look at our continuing sample using async/await:
This section of code does EXACTLY the same thing as the previous promise chaining code did, and nearly the same as the original callback hell code (the difference there being in how an error is handled). It is easy to see what this code is expecting to do — it will open a file, write some data, and close the file. Very simple.
What is less visible, until you understand the await keyword, is that each of those await keywords receives a Promise from the right-hand side and it will take the rest of the code that follows, wrap it up in a callback, and schedule it to execute at the completion of the asynchronous operation. This means that as soon as fs.openPromise()
returns its Promise to the await, your function stops executing and the event loop managing your thread is free to go do other things — like handle a click event or some other IO completion event.
Beware! There are serious race condition concerns with this style! In JavaScript, we get to live in a world where we know that no other parts of the code are executing in the middle of our function — our function will run from start to finish without interruption or interleaving of other work. The await keywords change this! At each await keyword, we return to the event loop where other pending operations start executing. We want this to happen, but you must keep synchronization concerns in mind. These issues exist in both of the other two styles, but it is easier to see these boundaries because you are explicitly setting callbacks or promise completion handlers. I use JavaScript in my examples, but these concerns exist for any language using the async/await.
I am an avid fan of async/aswait! The orders-of-magnitude improvement in code readability and reasoning it enables is outstanding. Kudos to the people who came up with the technique — you did a great thing for your fellow developers! If you are doing asynchronous development, this is the only technique you should use unless you have a good reason why you can not.
✉️ Subscribe to CodeBurst’s once-weekly Email Blast, 🐦 Follow CodeBurst on Twitter, view 🗺️ The 2018 Web Developer Roadmap, and 🕸️ Learn Full Stack Web Development.