Learn › DSA Patterns › In-Place Linked List Reversal › Case Study
Reverse Linked List:
4 Bugs to a Clean Solution
Not a tutorial — a real debugging log. Five attempts, four distinct bugs, each one reproduced with a concrete failing input before it was fixed.
The Problem
LeetCode 206 — Reverse Linked List
Given the head of a singly linked
list, reverse the list in place
— every node's next pointer
must flip to point at the node that came before it — and return the
new head.
Input
1→2→3→4→5→null Output
5→4→3→2→1→null Input
[1,2] (two nodes) Output
[2,1] Input
[] (empty list) Output
[] The Journey
Attempt by Attempt
Every bug below was reproduced with a real failing input before being fixed — not just described.
function reverseList(head: ListNode | null): ListNode | null {
let prev = null;
while (head) {
prev = null; // reset every pass
const temp = head.next;
head.next = prev;
prev = head;
head = temp;
}
return prev;
} Hand-tracing 1→2→3→null on the very first attempt shows the reversed list never actually forms — every node ends up pointing to null instead of the previous node.
prev is the one piece of state that must survive between iterations — it's the growing reversed list. Resetting it to null at the top of every pass throws that accumulated state away before it's used, so head.next = prev always writes null instead of the real previous node.
Proof — trace on 1→2→3→null:
iter1: prev reset→null. temp=N1(2). N0.next=null (prev). prev=N0(1). head=N1(2). iter2: prev reset→null AGAIN. temp=N2(3). N1.next=null (prev) — not N0! prev=N1(2). head=N2(3). iter3: prev reset→null AGAIN. temp=null. N2.next=null (prev). prev=N2(3). head=null → exit. return prev = N2(3). Expected: 3→2→1→null. Got: N0.next=null, N1.next=null, N2.next=null — three disconnected single nodes, only node 3 returned.
Declare prev once, before the loop, and never reset it inside — it must carry the reversal forward across every iteration.
function reverseList(head: ListNode | null): ListNode | null {
let prev = null;
while (head) {
const temp = head.next;
head.next = prev;
prev = head;
head = temp.next; // temp already IS the next node
}
return prev;
} prev now persists correctly across iterations — but stress-testing shows a node silently disappears from the traversal, and the function eventually throws.
temp was saved as head.next specifically so head = temp advances by exactly one node. Reading .next off temp again jumps an extra node ahead each pass — one node's link is never touched at all — and once temp itself becomes null near the end of the list, temp.next throws instead of the loop ending cleanly.
Proof — trace on 1→2→3→null:
iter1: temp=N1(2). N0.next=null (prev). prev=N0(1).
head = temp.next = N1.next = N2(3) ← node 2 is skipped entirely, its .next is never reversed!
iter2: temp=head.next=N2.next=null. N2.next=prev=N0(1). prev=N2(3).
head = temp.next = null.next → CRASH: Cannot read properties of null (reading 'next')
Expected: 3→2→1→null. Got: TypeError before the function can return, and node 2 was silently dropped from the traversal. temp already IS the next node — advance with head = temp, not head = temp.next.
function reverseList(head: ListNode | null): ListNode | null {
let prev = null;
while (head) {
const temp = head.next;
if (temp) {
head.next = prev;
prev = head;
head = temp;
}
}
return prev;
} Pointers advance correctly now — but running on any real list never returns; it hangs.
The if (temp) guard was likely added defensively, but it gates the very steps that make the loop progress, including head = temp. Once temp is null (the last node), the entire body — the reversal, the advance of prev, and the advance of head — is skipped. head stays frozen on the last node forever, and while (head) never becomes false.
Proof — trace on 1→2→3→null:
iter1: head=N0(1). temp=N1(2). temp truthy → N0.next=null, prev=N0, head=N1(2). iter2: head=N1(2). temp=N2(3). temp truthy → N1.next=N0, prev=N1, head=N2(3). iter3: head=N2(3). temp=null. temp falsy → guard body SKIPPED entirely. head still = N2(3). iter4: head=N2(3) (unchanged). temp=null again. guard skipped again. head still = N2(3). ...repeats forever — head never becomes null, while (head) never exits. Expected: return 3→2→1→null. Got: infinite loop, the function never returns.
Remove the guard entirely — the loop's own while (head) condition already handles termination correctly. Nothing inside the body should be conditionally skipped.
function reverseList(head: ListNode | null): ListNode | null {
let prev = null;
while (head) {
const temp = head.next;
head.next = prev;
prev = head;
head = temp;
}
return head; // head is null here!
} The reversal logic itself is finally correct — every link flips exactly once — but the function always returns null, no matter the input.
The while (head) loop can only exit once head has become null — that's the exit condition, by construction. Returning head at that point always returns null, discarding the reversed list that prev spent the whole loop quietly building. head was never the answer; it was only ever the variable steering the loop forward.
Proof — trace on 1→2→3→null:
iter1: temp=N1(2). N0.next=null (prev). prev=N0(1). head=N1(2). iter2: temp=N2(3). N1.next=N0 (prev). prev=N1(2). head=N2(3). iter3: temp=null. N2.next=N1 (prev). prev=N2(3). head=null → loop exits (this is WHY it exits). return head → null. Expected: 3→2→1→null (the correctly-built reversed list, sitting in prev). Got: null, every time, regardless of input.
Return prev, not head. Ask: which variable is my answer, and which was just steering the loop?
// O(N) time, O(1) space — iterative three-pointer reversal
function reverseList(head: ListNode | null): ListNode | null {
let prev = null;
while (head) {
const temp = head.next; // 1. save the rest of the list
head.next = prev; // 2. reverse the link
prev = head; // 3. advance prev
head = temp; // 4. advance head
}
return prev; // head is null — prev holds the answer
}
// 1→2→3→null (no cycle)
console.log(reverseList(list)?.value); // → 3 All four issues are resolved: prev persists across iterations, head advances by exactly one node via temp (not temp.next), no conditional guard skips the loop's own progression, and the function returns prev — the pointer that actually carries the reversed list.
Proof — full trace on 1→2→3→null:
Initial: prev=null, head=N0(1). iter1: temp=N1(2). N0.next=null (prev). prev=N0(1). head=N1(2). iter2: temp=N2(3). N1.next=N0 (prev). prev=N1(2). head=N2(3). iter3: temp=null. N2.next=N1 (prev). prev=N2(3). head=null → loop exits. return prev = N2(3) → 3→2→1→null ✅ Also verified: empty list ([]) returns null immediately; single node ([1]) returns that same node unchanged.
Interactive
Reversal Visualizer
Watch prev and curr flip every link on 1→2→3→null step-by-step.
Initial: prev=null, curr=1. List: 1→2→3→null.
Lessons Learned
Key Takeaways
Trace a small example by hand before trusting the code
Hand-tracing 1→2→3→null across all four attempts is what surfaced every one of these bugs — the reset, the skipped node, the hang, and the wrong return value.
State that must persist across iterations lives outside the loop
prev is the accumulating answer. Reset it — or re-declare it — inside the loop body, and every iteration starts from scratch instead of building on the last one.
temp already IS the next node — don't call .next on it again
temp was saved as head.next for exactly one reason: to let head = temp advance by one node. Reading .next off it a second time skips a node and eventually crashes on null.
Never let a guard skip the loop's own progression
An if (temp) check that wraps the entire body — including the line that advances head — stalls the loop on the last node forever. Guards should protect against invalid state, not gate the loop's ability to move forward.
Ask which variable is the answer, and which was just steering
head reaching null is the reason the loop exits — it was never meant to hold the result. prev quietly accumulated the real answer the entire time.
Final Result
The Verified Solution
Three pointers, one pass — save what's ahead, flip the link, advance both.
// O(N) time, O(1) space — iterative three-pointer reversal
function reverseList(head: ListNode | null): ListNode | null {
let prev = null;
while (head) {
const temp = head.next; // 1. save the rest of the list
head.next = prev; // 2. reverse the link
prev = head; // 3. advance prev
head = temp; // 4. advance head
}
return prev; // head is null — prev holds the answer
}
// 1→2→3→null (no cycle)
console.log(reverseList(list)?.value); // → 3 | Solution | Time | Space | Verified |
|---|---|---|---|
| Iterative Three-Pointer Reversal (Attempt 5) Verified ✓ | O(N) | O(1) | Multi-node, two-node, empty, and single-node cases |
Reuse This
Template: In-Place Reversal (prev / curr / temp)
The generic shape behind this solution — adapt it to any "reverse this chain" question.
function reverseList(head):
prev = null
while head is not null:
temp = head.next // 1. save the rest of the list first
head.next = prev // 2. reverse the current link
prev = head // 3. advance prev forward
head = temp // 4. advance head forward
return prev // head is null here — prev holds the answer When to reach for this
Any "flip the direction of this chain" question on a linked structure — O(1) space beats copying into a stack or array.
The trap this journey hit
Losing track of which variable is the answer (prev) versus which one merely steers the loop (head) — the same mix-up caused three of the four bugs above.
Test Yourself
Quiz: Check Your Understanding
Your score
0 / 6
Keep Learning
Bugs are part of the process 🎉
Four real bugs, four real fixes — that's what getting to a correct in-place reversal solution actually looks like.
Related: try LeetCode 92 — Reverse Linked List II (reverse only a sublist) or LeetCode 234 — Palindrome Linked List (uses this exact reversal as a building block).