No magic Babel

A scroll story · 2026

There is no magic in Babel

Babel turns new JavaScript into old JavaScript. It looks smart, but it doesn't understand code. It builds a tree, edits the tree and prints it back as text, following rules that people wrote, step by step. Scroll down and watch it happen to one line of code.

Input

Text

It's just text

Here's the line we'll follow: a let and an arrow function, two things very old browsers don't know. To Babel, at first, this is only a string of characters.

Step 1 · Parse

Text → tokens

First the parser cuts the text into tokens, the words of the language: let, greet, =, (, name, ), => and so on. No meaning yet, just pieces.

Tokens → tree

Then it groups the tokens into a tree, the AST. Each piece of the program becomes a node, and a node keeps its parts in named fields: a declaration has an id and an init, a + has a left and a right.

Hover or tap a node to see its piece of the code. Babel has its own parser; this page uses acorn. Same idea, same kind of tree.

A node is a plain object

Nothing clever lives in the tree. This is the VariableDeclaration node, as it is in memory. The word let is just a string in a field called kind.

Step 2 · Traverse

Meet the walker

The walker visits every node. It enters, goes down into the children, then leaves. It doesn't know what any node means. It carries a rulebook with two rules and checks whether a rule wants the node it's standing on.

  1. let → var
  2. arrow → function

Rule 1: let → var

The rule wants VariableDeclaration nodes. It has one step: if kind is "let", write "var".

VariableDeclaration(node, { step }) {
  if (node.kind !== "let") return;
  step("Change kind from let to var", () => {
    node.kind = "var";
  });
}

Rule 2: arrow → function

This rule wants ArrowFunctionExpression nodes, in two steps. One: change the node's type. Two: a function needs { } and return, so wrap the old body in two new nodes. Then the walker walks into the new nodes too.

Then it just keeps walking

No rule wants the other nodes, so the walker only enters and leaves them, all the way back up to the top. Babel calls these two moments “enter” and “exit”.

Step 3 · Generate

Tree → text

The printer walks the edited tree once more. For each node type it has a few lines that write text: a VariableDeclaration writes its kind, then its parts, then ;. It doesn't care that the tree was edited.

That's all of it

Parse, traverse, generate. Real Babel knows many more node types and has hundreds of rules (it calls them plugins), but each works like these two: someone wrote down the steps for one piece of new JavaScript.

Bonus level

Babel is not almighty

A rule does exactly what its steps say, nothing more. Rule 2 has two steps and no idea what this means. Inside an arrow function, this is the this of the function around it. Inside a function, it isn't. So this code breaks:

Input

read() returns 1

Rule 2 as above: two steps

read() returns undefined. Broken.

Rule 2 with two more steps

read() returns 1 again

Babel's rule isn't smarter. It has more steps, written by people who hit this bug before:

  1. Find every this inside the arrow
  2. Add var _this = this; to the function around it, and use _this instead
  3. Change the type to FunctionExpression
  4. Wrap the body in { return … }

The last of the three outputs is exactly what Babel prints for this code. And it's still not everything: arguments, super and new.target behave like this, and each needs steps of its own.

It's not an LLM

Babel doesn't read your code and decide what you meant, and it doesn't try something and check if it worked. Same input, same rules, same output, every time. Code that no rule wants passes through unchanged. Code the printer has no lines for stops mine with an error:

Check it yourself: Babel's arrow function steps · Babel plugin handbook · AST Explorer

Inventory

The whole compiler

Everything on this page ran in your browser just now, from these files: a few hundred lines of TypeScript, without comments and blank lines. Parsing is acorn's work.

Continue?

Run it yourself

The same steps, printed in your terminal. You need Node.js 22.18 or newer.

  1. Get the code

    git clone https://github.com/slimcandy/no-magic-babel
    cd no-magic-babel
    npm install
  2. Watch the three steps

    npm start
  3. Run the tests: the output must parse as old JavaScript and do the same thing as the input

    npm test