Imagine a chain of paper clips. Each clip is hooked to the next one — but only the next one. There's no way to reach backward. If you want the fifth clip, you start at the first and follow the chain forward: one, two, three, four, five. Skip ahead? Impossible. Go backward? Equally impossible. That's a singly-linked list: a sequence of nodes where each node stores a value and a single pointer to the next node. Nothing else.
In code, that looks like this:
interface ListNode { val: number next: ListNode | null}Two fields. That's the entire interface. Tap each compartment to see what it does:
Tap a compartment to open it.
Each node has exactly one outgoing connection — its next pointer. The last node's next is null, signaling the end of the chain. There is no .prev pointer, no array index, no .length property, no way to jump to the middle. The only way through a singly-linked list is forward, one .next at a time. This is the constraint that makes everything hard.
Tap any node below to see what's reachable from that point — and what's already behind you:
Tap any node to see what it can reach.
Reversing an array is straightforward — swap the first and last elements, then the second and second-to-last, working inward from both ends. Two pointers, converging toward the center, done in n/2 steps. But a singly-linked list doesn't give you both ends. You have the head. That's it. You can march forward. You cannot start from the back and work inward because there is no back — not one you can reach, anyway. Every connection is a one-way street, and they all point the same direction.
So you have to flip each arrow one at a time, working front to back. And here's the catch: when you overwrite a node's next pointer, you're not copying the old value somewhere safe. You're replacing it. The old connection — the only path to every node after the current one — is gone. If that pointer was the only way forward, and you just overwrote it...
Picture three nodes: A → B → C. You want to flip A's arrow so it points backward. You write A.next = null — done, A now points the right way. But A's next was the only connection to B. That connection is gone. B and C still exist somewhere in memory, but nothing in your code can reach them anymore. See for yourself:
There's one move that saves you: write the address down on a sticky note before you overwrite it. That's all a local variable is — a sticky note with an address on it. Stick a sticky note on node 2's location before you erase A's arrow, and node 2 is still reachable even after the old path is gone. No sticky note, no recovery.
Below is a five-node linked list. Your job: reverse the first connection. Tap the arrow between node 1 and node 2 and see what happens — no sticky note yet, just the raw overwrite.
What could go wrong?
You just watched nodes disappear. Not because of a bug in the visualization — that's what actually happens when you overwrite a next pointer without saving it first. In the interactive, writing curr.next = prev replaced the only path from node 1 to node 2. Once overwritten, node 2 — and everything after it — became unreachable. Not deleted from memory. Not corrupted. Just unreachable: the nodes are still allocated on the heap, but nothing points to them anymore. In a garbage-collected language, they'll be cleaned up eventually. In C, you just leaked memory. Either way, the data is gone from your perspective.
Think of pointer assignment as walking through a one-way door. On the other side is a new value — in this case, prev. The moment you step through, the door swings shut behind you. The room you left — the one that held curr.next's old value — is sealed off.
Unless you took a photo of the room before you walked through:
Left room holds the current value of curr.next (→ B). Right room holds prev.
That's what next = curr.next does. It sticks a sticky note on where curr.next currently points — before you overwrite it. With that sticky note stashed in a local variable, you can safely walk through the door. The old room is still sealed off, sure. But you have a sticky note with node 2's address on it. You know exactly where node 2 lives, because you wrote its address on a note before you wrecked the only signpost.
Step through the full save-and-sever mechanism on a three-node chain. Each tap fires one pointer move — watch how the sticky note survives the sever:
A points to B, B points to C. Every connection is one-way.
These two lines must execute in exactly this order:
const next = curr.next // 📸 take the photocurr.next = prev // 🚪 walk through the doorSwap them, and the photo captures prev instead of the forward reference — completely useless. Toggle between the two orderings to see why:
next = curr.next// saves B ✓curr.next = prev// severs safelyThe bookmark captures B before the link is severed.
This ordering constraint is the invariant: save before you sever. One line of defense between a clean reversal and a shattered list.
You proved it yourself. Four nodes, four iterations, same two-step rhythm every time. First the bookmark, then the flip. The interactive wouldn't let you do it the other way — and now you know why.
This isn't a trick specific to textbook list reversal. Every variant of the problem — reversing a sublist from position m to n, reversing in groups of k, even the recursive approach — has the same invariant at its core. The boundaries change (where you start and stop reversing), the bookkeeping gets more complex (tracking reconnection points at the edges of the reversed section), but the fundamental rhythm never does: save the forward reference, then sever the link. The four-step loop body you'll learn next is built directly on top of this two-step core.
Break the ordering and you lose nodes. Keep it and the list stays intact — no matter how many arrows you flip, no matter how long the chain.
The loop you just built has to handle two degenerate inputs cleanly.
First, the empty list: head === null.
curr is initialized to head, so curr starts as null, the while (curr !== null) guard fails immediately, and the function returns prev, which is still null. Nothing shatters because nothing was ever touched. That's the correct answer — reversing an empty list produces an empty list.
Second, the single-node list.
head points to one node whose next is already null. One iteration runs: save null, overwrite curr.next to null (no change, it was already null), advance prev to the node, advance curr to null. The guard fails on the second check, and we return the same single node. Both the empty list and the single-node list fall out of the invariant without any special-casing — a sign the logic is clean.
There are three other ways to reverse a linked list, and each makes a different trade-off.
The recursive approach walks to the tail, then unwinds the call stack while flipping pointers on the way back. It's elegant — three lines — but uses O(n) stack space and blows up on long lists. Recursive solutions are favored in interviews that want to test divide-and-conquer fluency, but they're not the default in production.
The stack-based approach pushes every node onto an explicit stack, then pops them back and rewires next pointers during the pop phase. Same O(n) space cost, easier to reason about than recursion, but still twice the memory of the iterative version.
Doing it iteratively with three pointers — the version you just proved — uses O(1) extra space and runs in a single pass. That's why writing it iteratively is the canonical answer in every interview and every production codebase: the sticky-note trick costs nothing extra but buys you the whole reversal.