Sitting comfortably? Then I shall begin…

…And do not forget, if you like this post, clap 👏 , share, and follow me on Twitter 🐦! It means a lot!

1. What is React Fiber?

Briefly, React Fiber is a rewrite of React’s core reconciliation algorithm to provide greater control over the render cycle. Fiber’s exciting features depend mostly on a new superpower: the ability to pause and resume in the middle of a render cycle. Some of those features are:

Time slicing : a single render cycle is broken into small chunks which can be processed at different times. A high priority render can interrupt a low priority render, which can be resumed when the high-priority render is complete.

: a single render cycle is broken into small chunks which can be processed at different times. A high priority render can interrupt a low priority render, which can be resumed when the high-priority render is complete. Suspense : a component can make an asynchronous request for data, suspend any further processing of itself until that request is complete, and then resume with the data.

: a component can make an asynchronous request for data, suspend any further processing of itself until that request is complete, and then resume with the data. Error boundaries: errors can be caught anywhere in the component tree and rendering can be resumed from the point at which the error was caught.

If any of this is new to you, go watch Lin Clark’s A Cartoon Intro to React Fiber, and follow it up with Dan Abramov’s Beyond React 16. For the implementation background, check out Andrew Clark’s react-fiber-architecture repository.

The key takeaway from the react-fiber-architecture repository is the following:

Fiber is a reimplementation of the stack, specialized for React components. You can think of a single fiber as a virtual stack frame. The advantage of reimplementing the stack is that you can keep stack frames in memory and execute them however (and whenever) you want.

The exotic language features mentioned above — continuations, algebraic effects, coroutines and fibers — provide structured ways of exercising that extra control over stack frame execution. They are not supported by JavaScript’s native stack, but can be supported, in a manner of speaking, by the specialised “virtual” stack in React Fiber.

Of course that phrase “in a manner of speaking” hides a lot of ambiguity.

So let us see what we can do about that.

It is useful to begin by describing what these language features look like in the abstract (theory) before we look at their transmogrification into React (practice). To give this plunge into theory some context, take a look at this informal proposal for a new JavaScript language feature posted by Sebastian Markbåge and motivated by React. We shall dissect the terms in this proposal one by one.

(Side-note: this is not a picture of Sebastian Markbåge; I don’t know who this is 🤷.)

Continuations are the most basic of the various language features we are considering, and the basis of the others, so let us begin there.

2. Continuations

A continuation is a control flow primitive, which means it is in the same category of things as while loops, if/else clauses, and functions: it is something we can use to avoid having to write code in the order we want it to be executed.

Specifically, a continuation is commonly defined as an abstraction to represent “the rest of the computation”…whatever that means.

This standard definition confuses me. Do we not already have an abstraction to represent “the rest of the computation”? It is called the stack.

The stack is a list of jobs. When the stack is empty, it means the program has terminated. Each job is represented on the stack by a “stack frame”, which enables the data required for a particular function call to be efficiently located and deallocated when no longer needed.

Building and completing the stack. Pointers keep track of the current frame.

The defining feature of the stack is that stack frames are ordered by their physical location in memory. Each frame is placed at a memory address following the previous frame. This sequential storage is what makes the stack such an efficient way to queue and process jobs, since the data needed for the current computation can be located simply by keeping track of the end of the stack. Sequential ordering has an important limitation, however: we cannot change the order of execution of jobs (frames) once the stack has been built up. What goes up must come down, and in exactly the same order.

Now, let us imagine for a moment that instead of storing frames sequentially in memory we stored them at random locations on the heap. We would need some other way of defining the order in which frames should be processed: we would need to include a pointer in each frame to point to the location of the next. In other words, we would store our queue of jobs as a linked list. In order to return from a subroutine, our runtime would use these pointers to jump to the next stack frame.

Those pointers that point to the frame to continue with when the current one is done… let us call them continuation pointers.

What is the major advantage of linked lists? Insertion and reordering of nodes is easy.

So, let us suppose that our language has the ability to “reify” the process of returning from a particular subroutine and give it to us as a first class object, a version of return that we can store, pass around, and eventually call as if it were a function. This ability would be quite pointless if the behaviour of return were constrained by the order of the stack, since in that case the only possible return would be to the calling function, just as with the keyword return . If return worked with continuation pointers, however, we could pass it around at will, and invoke it from anywhere to jump to the call frame indicated by the pointer. We could use it to skip forwards in the call chain in order to skip queued tasks, or to skip backwards in order to rerun tasks that had already completed.

If we had such a magical return function, it would be called a continuation.

At the language level — for example, in Scheme and some versions of Ruby — continuations are usually accessed through a function called callcc (“call with current continuation”). This is what callcc looks like in fictional JavaScript:

callcc reifies the current continuation at the point it is called, and binds it to cont to be handled inside a callback. From there, we can use the reified continuation however we like. This example would log hello , call the continuation, which would restart execution at line 3, log hello again, and so on, in an infinite loop.

Like JavaScript’s return , callcc gives us the ability to pass data into the next call frame. Any data passed in is then accessed inside the continuation as the result of the call to callcc , like this:

In this case, we will first see undefined logged, and then hello in an infinite loop.

You can begin to see now how continuations might be used within React Fiber to implement something like time slicing. React has a list of jobs that it needs to do, which are waiting on a queue. It pulls a job off the queue and begins processing it within an interval set by the scheduler. When the deadline expires, it can push the last saved continuation back onto the queue and resume it in the next interval.

Simplified model of time slicing.

Delimited or undelimited?

The continuations captured by callcc are undelimited continuations. A continuation that captures the whole call graph up to the point it is reified is undelimited. It will continue to run until the program exits. Note that undelimited continuations are rather different from functions in this regard, since a function will always return before the program exits and so produce a result that can be handled within the program. Undelimited continuations never return.

Using undelimited continuations for control flow is a bit like using callbacks in Node.js. If we want to handle the result of a continuation, we can only do so inside that continuation, so we need to pass a handler into the continuation when we invoke it. The result resembles the famous pyramid of hell:

Another way of stating the problem: undelimited continuations do not compose.

Suppose instead that our continuations did not capture the whole call chain, but only a certain section up to some delimiter. In other words, suppose our linked list of jobs had an entry/exit point which, instead of terminating the program, returned a computed value to another sequence of jobs; since those jobs are beyond the delimiter, they do not need to handle continuations and might as well be on the stack.

Model of delimited continuations

We need some operators to both mark a delimitation and reify a continuation. The most common delimited continuation operators — you can find them in Racket and some versions of Scala — are shift and reset . They look something like this (again, in fictional JavaScript, of course):

First, reset marks the delimitation. Then shift does three things:

It clears the current continuation up to the enclosing reset . In this example, that continuation is * 2 . It binds that cleared continuation to cont . It executes the body of the function passed to shift .

Confusingly, the result of the call to reset is whatever was returned by the callback passed to shift . Inside the shift callback body we can use the reified continuation as if it were a normal function, where the return value of the function is the result of running the continuation. Here we compose its output multiple times to octuple a number.

Heaven knows that shift can be quite mind-bending the first time you see it. The key to following it is to first ignore everything else in the reset callback (i.e. the continuation) and treat the function passed to shift as the ‘real’ content of the reset callback. After all, whatever this function returns is what will finally be returned from reset ; and if the shift callback does not call cont , that continuation will never in fact be executed. shift is aptly named in the sense that it reifies the body of the reset callback, removes it, and shifts it into the body of the shift callback in the form of a function.

With composable/delimited continuations we can do some interesting things. For example, we can refactor callback-style control flow into something resembling async/await or generators. In the example below, we pass the reified continuation directly into an asynchronous function as its callback. Note that the shift s appear to be successive, but are in practice nested, since the second is cleared and passed to the first inside its continuation.

The resemblance between this code and async/await or generators is no mere chimera, as we shall see in the next section.

Stackful or Stackless? One-shot or multi-shot?

It turns out that yield is in another, albeit rather more restricted, delimited continuation operator.

The best way to appreciate the proximity of yield to shift is to examine a generator implemented using shift and reset . The API of the generator will be as follows. The code below logs 1 , then 2 .

The implementation looks like this:

Let us break this down. It is easiest to read from the bottom up:

Line 16: we want our generator to return when we call next , so we wrap next in a reset .

, so we wrap in a . Line 12: the first time next is called, we simply execute the function passed into the generator constructor, and pass in yield .

is called, we simply execute the function passed into the generator constructor, and pass in . Line 5: When we call yield , we call shift , save the continuation at that point in resumeGenerator , and return whatever was passed to yield . reset returns what shift returns, so whatever was passed to yield is also passed back from next .

, we call , save the continuation at that point in , and return whatever was passed to . returns what returns, so whatever was passed to is also passed back from . Line 16: the next time we call next , we call the continuation that was saved in the previous step in order to resume execution of the generator at that point.

So you can see that yield is a rather thin wrapper over shift , just adding a few restrictions:

yield returns immediately after capturing the continuation.

returns immediately after capturing the continuation. The only way to activate the continuation is to call next .

. Once you have called next , you cannot backtrack; the continuation is overwritten.

That last restriction is what is meant by one-shot in Sebastian Markbåge’s One-shot Delimited Continuations with Effect Handlers proposal. A one-shot continuation can be called at most once. Generally, one-shot continuations are easier on the runtime, since the continuation can be overwritten in place.

Compare the simplicity of the implementation of generators using shift and reset with the implementation of generators in a language without support for delimited continuations, ES5. The generator body has to be divided up by the transpiler into case blocks at every yield ; flow is then controlled by incrementing some state ( _context.next ) to determine which block to execute. Ugh.

Generators as they are implemented in JavaScript have another crucial restriction, which does not exist in the shift-reset-based generator above. JavaScript’s yield — analogously to return for ordinary functions — can only be called inside the top level of the generator body. By contrast, shift , or yield implemented with shift , can be invoked at any depth of call to capture the entire call chain back up to reset . Sometimes we say that JavaScript generators save stackless or single-frame continuations, whereas shift saves stackful or multi-frame continuations.

It is possible to work around the single frame limitation in JavaScript by invoking a generator within another generator, but for deeply nested calls, such as a React component tree generates, the syntax overhead becomes obstructive, as all intermediate functions need to be generators. This issue is one of the motivations behind Sebastian Markbåge’s proposal.

3. Effect Handlers

Finally, it is time to take a look at the “effect handlers” part of that proposal. Although they sound complicated, “algebraic effects” are just a small refinement of delimited continuations. First, note that shift behaves a bit like throw in that both clear the current delimited continuation (with throw the delimiter is try ). There are two major ways in which shift differs from throw . Namely, with throw :

The continuation is forcibly discarded instead of reified and made available. Control is passed to a handler defined outside of the delimiter instead of to a function defined inline.

Suppose that we had instead decided errors should be handled like this:

This pattern seems a bit obtuse, and yet it is more or less what happens with shift . This is, I think, one reason shift at first appears so strange. Since the current continuation is cleared, it makes sense to jump over it and pass control to a handler that is defined outside it. The ‘shift’ in control caused by clearing the current continuation is now reflected by a physical displacement of code:

What if shift behaved in a similar way? What if, on calling shift , control jumped to a handler, and the continuation was dealt with there, instead of in-line?

Since with this syntax the handler is no longer in scope, we also need the ability to pass extra data into it. So let us add that, and let us rename reset and shift to handle and effect . Let us also pass the effect into the function to signify that handlers are bound to their effects:

The result is the language feature called “algebraic effects”. “Algebraic” just means that some laws apply about how these effects compose. The important part is the “effects”. What is an effect? It is nothing other than a custom delimited continuation operator. Instead of calling the generic shift we call an effect which behaves just like shift , but embeds some particular user-defined way of handling the continuation. Since we can define handlers, we can encapsulate particular ways of handling delimited continuations and make them easily reusable. For example, we can easily implement async/await using algebraic effects:

In this case, all our handler has to do is pass the continuation to the function thrown into the handler as its callback. Of course we could embed much more specific behaviour if we wished:

Here we have bound the handlers to particular functions. This degree of encapsulation is probably too fine, but illustrates the idea. Andrej Bauer, the creator of a language for algebraic effects, Eff, puts it concisely:

eff : shift/reset = while/if-then-else : goto.

4. Coroutines and fibers

Coroutine is another term that pops up frequently in the context of React Fiber.

So what is a coroutine? There are several answers to this question, depending on the context, but generally speaking a coroutine is a generator with a bit of extra functionality. The following might be referred to as coroutines:

A generator (producer) that can also consume values. JavaScript generators can consume values through .next and so by this definition are coroutines.

and so by this definition are coroutines. A generator (called a a semicoroutine in this context) that can yield to other generators. Again, JavaScript generators are coroutines by this definition, since they can call other generators with yield* .

in this context) that can yield to other generators. Again, JavaScript generators are coroutines by this definition, since they can call other generators with . A generator that can resolve asynchronous values, like async/await. JavaScript generators cannot do this by themselves, but can be wrapped to have this functionality. This meaning is the most common meaning of “coroutine” in the JavaScript world. For example, before async/await we had co and Bluebird.coroutine, which were async/await implementations based on generators.

and which were async/await implementations based on generators. A generator that can yield with a stackful continuation, like shift . With React Suspense we can pause reconciliation at any depth. So the reference to coroutines in Andrew Clark’s tweet would seem to refer to this idea of being able to resume a deeply nested call frame.

If Suspense is based on coroutines (under the final definition), why is React 16 called React Fiber and not React Coroutine? “Fiber” is frequently used as a synonym of “coroutine”. That may be what Andrew is doing in his tweet, and in most respects the last definition of coroutine given above covers what is meant by a fiber in React. But there is a relevant difference between the two, nicely described by this document. When a coroutine yields, control is passed to the caller and handled by application code. When a fiber yields, control is passed to a scheduler which determines what to run next. Fibers are therefore controlled at the level of the operating system or framework.