Copperwall Blog

About

Book Review: You Don't Know JS: Async & Performance

I recently read You Don't Know JS: Async & Performance by Kyle Simpson. As someone who's struggled with promises, callbacks, and generators when writing asynchronous JavaScript (which ends up being most of the JavaScript), I thought it would be a good investment to better understand how they really work and understand their nuances.

Introduction

The book starts off with an introduction to the event loop and how that relates to some parts of your JS program are run "now" and some parts are run "later". It's explained that later can mean immediately after the current function ends, or it could make multiple seconds from now in the case that "later" portion has to wait for a network request or a given event to be triggered. It also explains how concurrency happens at the function level and not at the statement, meaning that a function will run from start to finish without being preempted by another function call. Knowing this, you can write functions assuming that they might not always be executed in the same order, but you won't need to guard against values changing out from under you once a function has started.

Callbacks

From there it explains how asynchronous code works by using callbacks. The main complaints are that callbacks do not look or act like synchronous code you would see in other languages, so it's hard to reason about them, and that it's hard to trust the code the callback is passed to to evaluate it correctly. They also require you to implement your own latches and gates for cases where you want to either wait for all of a list of callbacks to complete or one of a list of callbacks to complete. A common example is sending off multiple network requests and wanting to wait until all requests have completed before continuing on in the program. Each time a callback is run, it has to signal that it's run to some shared scope between the callbacks and check if it's the last one to complete. This can get complicated very fast. The argument against trust specifies that you as a developer usually don't have control of how your callback function is called, including how many times it's called, or if the function is called in the correct context. This is especially true when passing callbacks to third party libraries.

Promises

The next chapter introduces promises and how they address most of the concerns that were made about callbacks. The chapter introduces promises first as future values that eventually resolve into a fulfilled value or a rejected value. It also highlights the importance that once a promise is resolved, it's value is immutable. This aspect solves the callback trust issue where a third party library could unexpectedly call a callback function multiple times. Once a promise is resolved, further calls to its resolve or reject functions are ignored. Further along in the chapter it goes over how to properly catch errors in a promise and in which cases an error can be swallowed. Promise.map and Promise.race are explained as solutions to the callback problems where you want to wait for either all asynchronous tasks in a list to complete, or just one to complete before moving forward. Some new things I learned from this chapter include using Promise.resolve and the difference between a promise being resolved, fulfilled, or rejected. Promise.resolve is helpful when you have a value that could be an actual value or a promise and want to guarantee that the value is a promise, and therefore asynchronous. If the value was already a promise, Promise.resolve will return that same promise. If the value was an actual value, Promise.resolve will wrap it in a promise that immediately resolves to that value. Promise.resolve can also be used to convert "thenable" objects, or objects that implement a then method, into actual promises. I haven't had much use for that functionality, but it's interesting that you can use Promise.resolve to clean up values that are promise-like into actual promises.

Generators

The chapter on promises was a logical continuation of the chapter on callbacks. The next chapter focuses on generators and takes a turn to discuss how we can write asynchronous code that looks synchronous. The introduction of this chapter discusses what a generator is, how it can be used for two-way message passing, and how to use an iterator to control the execution of the generator. There's a really fun "breakthrough" moment midway through the chapter that shows how you can write something like const res = yield request('https://...') inside of a generator function and effectively pause the execution of that function until the promise returned from request has resolved. The return value of request is yielded to an outer function that progresses through the execution of the generator function. That outer function then uses the two-way message passing functionality of generators to resume execution and pass in the resolved value from the promise to replace the promise expression that was yielded when the generator last stopped. I kind of glossed over a lot of stuff, but I really recommend reading through that chapter to see how generators can be used to write asynchronous code that behaves synchronously. The way generators work is very similar to the way async/await works in ES2016.

Performance

The next two chapters focus mostly on performance and benchmarking. The biggest takeaway I have from both of those chapters is to not try and outsmart the JS engine with tiny performance "hacks", because best case you make your code harder to understand, and worst case you might end up optimizing for one JS engine and degrading performance in all others. The gist was basically to not try and outsmart the JS engine. It also recommends benchmark libraries like Benchmark.js and jsperf.com, as opposed to homegrown solutions that don't take into account things like standard deviation and variance.

Appendix

The first appendix goes into detail about the asynchronous library written by Kyle Simpson called asynquence, while the second goes into detail about using generators for Communicating Sequential Processes, which is basically implementing channels from golang in JavaScript.

Overall Thoughts

Overall I liked this book a lot. The first two chapters did a great job of creating a base understanding of how asynchronicity works in JavaScript before venturing into promises. After the promises chapter, the book picks up a lot but gets more and more interesting. I'm probably going to read the generators chapter through the end of the book again to pick up anything I missed the first time.


Creative Commons LicenseMastodon