Coroutines: promises and futures 39 The latter approach is also referred to as the continuation-passing style. Each subtask calls a new one once it's finished. The difference between callbacks and promises is even more substantial under the hood. Y ou see, promises return a state machine that can be in one of three states: pending, fulfilled, or rejected. When we call timer(200) in the previous example, we get back a promise in the pending state. Now, the continuation-passing style does fix some of the issues related to callbacks, but it still retains a lot of them when it comes to complexity and the different ways of writing programs. However, they enable us to leverage the compiler to solve a lot of these problems, which we'll discuss in the next paragraph. Coroutines and async/await Coroutines come in two flavors: asymmetric and symmetric. Asymmetric coroutines yields to a scheduler, and they're the ones we'll focus on. Symmetric coroutines yield a specific destination; for example, a different coroutine. While coroutines are a pretty broad concept in general, the introduction of coroutines as objects in programming languages is what really makes this way of handling concurrency rival the ease of use that OS threads and fibers/green threads are known for. Y ou see when you write async in Rust or Java Script, the compiler re-writes what looks like a normal function call into a future (in the case of Rust) or a promise (in the case of Java Script). Await, on the other hand, yields control to the runtime scheduler, and the task is suspended until the future/ promise you're awaiting has finished. This way, we can write programs that handle concurrent operations in almost the same way we write our normal sequential programs. Our Java Script program can now be written as follows: async function run() { await timer(200); await timer(100); await timer(50); console. log("I'm the last one"); } Y ou can consider the run function as a pausable task consisting of several sub-tasks. On each “await” point, it yields control to the scheduler (in this case, it's the well-known Java Script event loop). Once one of the sub-tasks changes state to either fulfilled or rejected, the task is scheduled to continue to the next step.