Debugging Guide & Interview Hacks
The ability to find and fix bugs under pressure separates candidates who pass from those who don't. This page teaches you how to debug like a pro — in real interviews and on the job.
Bugs in interviews are normal and expected. What matters is that you find and fix them systematically, not that you write perfect code on the first pass. An interviewer watching you debug methodically is often a better signal than watching you write bug-free code in silence.
1. The Systematic Debugging Method
When your code fails a test case, apply this process. Panic is the enemy — structure is your friend.
| Step | Action | What to say out loud |
|---|---|---|
| 1. Read the error | Don't guess. Read the actual error message — type, line, expected vs actual output. | "The test case is [1,2,3] and expected 6 but I got 5. Let me trace through." |
| 2. Trace the failing case | Manually execute your code line by line on the failing input. Write down variable values at each step. | "i=0, sum=1. i=1, sum=3. i=2... ah, I should add nums[2] but my loop stops at n-1." |
| 3. Identify the divergence point | Find the exact line where the actual behavior diverges from expected. Binary search your code. | "Everything is correct up to line 8. The problem is on line 9 where I use i < n instead of i <= n." |
| 4. Fix and verify | Make the smallest possible change. Re-trace the failing case to confirm it now passes. | "Changed to i <= n. Re-tracing: i=0→1, i=1→3, i=2→6. Output is 6. Correct." |
| 5. Check for regression | Verify your fix doesn't break other test cases. Re-run your other examples. | "Let me also check the empty array case and the single-element case..." |
2. Bug Taxonomy — The Most Common Bugs
95% of bugs in coding interviews fall into one of these categories. Know them cold so you can find them fast.
| Bug Type | Classic Symptom | Signature Code | Fix |
|---|---|---|---|
| Off-by-one | Last element missed, or array index out of bounds | i < n should be i <= n (or vice versa) | Always trace n=1 and n=2. Ask: is the boundary inclusive or exclusive? |
| Wrong base case | Correct transitions, wrong answer for empty/single input | dp[0] = 0 should be dp[0] = 1 | Define dp[i] precisely in English. Verify dp[0] matches that definition. |
| Integer overflow | Negative answer, or wildly wrong large numbers | int a = 10^5; a * a overflows int | Use long for multiplication. Apply % MOD immediately after multiply. |
| Null/None pointer | Crash on empty input, single node, or leaf node | node.next.val when node.next is null | Always guard: if (node == null) return ... at the top of every tree/list function. |
| Wrong loop direction | 0/1 Knapsack giving wrong answer | Inner loop goes forward instead of backward | 0/1 Knapsack: inner loop must go right-to-left (backward). Unbounded: forward is fine. |
| Mutation of shared state | Backtracking returns wrong results after first path | Appending to path without removing on backtrack | Every path.append(x) must have a matching path.pop(). |
| Wrong comparison (Java) | Strings not matching even when they look equal | s1 == s2 instead of s1.equals(s2) | In Java, always use .equals() for strings. == compares references. |
| Modifying input array | First test passes, later tests fail mysteriously | Sorting nums[] in-place, corrupting the test case | Ask "can I modify input?" If not, sort a copy. |
| Returning wrong type | Returns null or undefined on valid input | Missing return statement in a branch | Write all return statements first. Check every code path has a return. |
| Graph: not marking visited | Infinite loop, stack overflow, TLE | BFS/DFS without a visited set | Mark visited before pushing to queue, not after popping. |
3. Edge Case Checklist
Check these for every problem before saying "I'm done."
| Edge Case | What to Check |
|---|---|
| Empty input | Empty array, empty string, null pointer — always test this first. |
| Single element | Array of size 1 — many off-by-one bugs reveal here. |
| All same values | [1,1,1,1] — deduplication logic, frequency logic, etc. |
| Already sorted | [1,2,3,4] — sorting algorithms must handle this in O(N). |
| Reverse sorted | [4,3,2,1] — worst case for naive sorts, two-pointer direction matters. |
| Negative numbers | Does your min/max initialization account for negatives? (Don't init max=0 if values can be negative.) |
| Zero | Zero as input value, zero as array element, modulo by zero. |
| Maximum constraints | N = 10⁵ or 10⁶ — does your solution TLE? Does it overflow int? |
| Cyclic inputs | Linked list with cycle, graph with cycle — infinite loop if not guarded. |
| All True / All False | For boolean logic — what if every element satisfies the condition? None does? |
| Duplicate keys | HashMap behavior when the same key is inserted twice. |
| Unicode / special chars | For string problems — does your code break on 'ä', emoji, or whitespace? |
4. The Dry Run — How to Trace Code Like a Pro
Never trace from memory. Write down every variable value at every step. If you're tracing in your head, you're guessing. Bugs live in the gap between "I think this is X" and "this is actually X."
// Input: nums = [2,7,11,15], target = 9 unordered_map<int,int> seen; for (int i = 0; i < nums.size(); i++) { int complement = target - nums[i]; if (seen.count(complement)) return {seen[complement], i}; seen[nums[i]] = i; } // Trace: // i=0: nums[0]=2, complement=7, seen empty → miss → seen: 2→0 // i=1: nums[1]=7, complement=2, seen has 2 → HIT! return [0, 1] ✓
// Draw this table on paper / whiteboard for complex problems // Example: prefix sum subarray sum = k // nums = [1, 2, 3], k = 3 // // i | nums[i] | cur | cur-k | cnt[cur-k] | cnt map | ans // -- | ------- | --- | ----- | ---------- | --------------- | --- // - | - | 0 | -3 | 0 | 0:1 | 0 // 0 | 1 | 1 | -2 | 0 | 0:1, 1:1 | 0 // 1 | 2 | 3 | 0 | 1 | 0:1, 1:1, 3:1 | 1 ← [0,2] sum=3 // 2 | 3 | 6 | 3 | 1 | 0:1,1:1,3:1,6:1 | 2 ← [2] sum=3 //
# Bug 1: Forgot path is mutable — .append() without .pop() def backtrack(idx, path): res.append(path) # BUG: all entries point to same list res.append(path[:]) # FIX: take a copy # Bug 2: Marking visited AFTER adding to queue (BFS) q.append(node) visited.add(node) # BUG: other neighbor might add same node before we process it visited.add(node) # FIX: mark BEFORE adding to queue q.append(node) # Bug 3: min init too high for negative values max_val = 0 # BUG: if all nums are negative, answer is wrong max_val = float('-inf') # FIX: use -inf
5. Interview Hacks — Things They Don't Teach You
| Hack | What it does for you |
|---|---|
| Return first, code second | Write the return statement before filling in the function body. Ensures you never forget to return anything. |
| State your invariant | Say "at every iteration, left contains all elements less than pivot." Interviewers reward candidates who reason about correctness, not just functionality. |
| Dummy head for linked lists | Create a dummy node that points to head. Eliminates all special-case code for empty lists and insertions at position 0. |
| Use n+1 sized arrays for DP | dp[0..n] where dp[0] is the base case. Avoids -1 index calculations and makes transitions cleaner. |
| Early return on edge cases | Put all null/empty checks at the top of the function. Keeps the main logic clean and removes special-case handling from loops. |
| Mark visited before enqueue (BFS) | Always mark a node visited when you add it to the queue, not when you process it. Prevents duplicate processing and infinite loops. |
| Two-pass algorithm | For problems that feel hard in one pass, think "what if I go left-to-right, then right-to-left?" Works for: Product Except Self, Trapping Rain Water, Candy. |
| Think about the complement | Instead of "how many subarrays satisfy X", count "total subarrays" minus "subarrays that don't satisfy X". Often easier to count the complement. |
| Reduce to a known problem | "This is just Two Sum but with triplets." "This is LCS but on an array." Recognizing the underlying problem is half the solution. |
| Use long/BigInteger proactively | Whenever you multiply two numbers that could each be up to 10⁵ or more, immediately switch to long. The cost is zero; the alternative is a wrong answer. |
6. Algorithm-Specific Debugging Checks
Binary Search
- Is the boundary
lo <= hiorlo < hi? Exact match needs<=; lower/upper bound needs<. - Is mid computed as
lo + (hi - lo) / 2? This prevents overflow. - Are the updates
lo = mid + 1andhi = mid - 1(for exact match) orhi = mid(for lower bound)? - Does the loop terminate? If lo and hi both update toward mid, it must. But verify: lo=5, hi=6, mid=5 — does your update move lo past hi?
Dynamic Programming
- Is
dp[0](ordp[0][0]) initialized correctly? Verify against your English definition. - Is the iteration order correct? If dp[i] depends on dp[i-1], iterate forward. If it depends on dp[i+1], iterate backward.
- For 0/1 Knapsack: is the inner weight loop going backward? If forward, you're allowing unlimited use of each item.
- Is the final answer
dp[n],dp[n][W], ormax(dp)? Be explicit.
Graph / Tree
- Did you handle the null/empty case?
if (!root) return ...orif (node == null). - Is the visited set initialized with the start node before the loop begins?
- For directed graphs: does the adjacency list represent directed edges only, or did you accidentally add both directions?
- For DFS: are you restoring state after recursion (backtracking)? If you modify a visited array or a path, you must undo the modification.
7. Language-Specific Debugging Cheat Codes
// Print vector for (int x : v) cerr << x << " "; cerr << endl; // Debug macro #define DBG(x) cerr << #x << " = " << (x) << endl DBG(n); DBG(v[i]); DBG(dp[3][4]); // Check for overflow before it happens // INT_MAX = 2,147,483,647 ≈ 2.1 × 10^9 // LLONG_MAX = 9.2 × 10^18 // Common values to remember // 1e9 + 7 → prime, used as MOD // 1e9 + 9 → second prime MOD // INT_MAX / 2 → safe "infinity" for shortest paths
// Print array System.out.println(Arrays.toString(arr)); System.out.println(Arrays.deepToString(grid)); // 2D // Print ArrayList System.out.println(list); // Common values Integer.MAX_VALUE // 2,147,483,647 — use Integer.MAX_VALUE/2 as safe infinity Long.MAX_VALUE // 9.2 × 10^18 // Gotcha: Integer.MAX_VALUE + 1 overflows to Integer.MIN_VALUE // Always use dist[u] + w < dist[v], never dist[v] > dist[u] + w when dist[u]=MAX_VALUE // Check for integer overflow risk // a * b overflows when both are ~50,000 (50000*50000=2.5*10^9 > INT_MAX)
# Print list nicely print(arr) print(*arr) # space-separated # Python integers never overflow — but float('inf') is useful INF = float('inf') dist = [INF] * n # No integer overflow in Python — 10**18 is fine # But watch out: in competitive programming, Python is 5-10x slower than C++ # Use PyPy if available, or optimize loops # Common Python gotcha: sys.setrecursionlimit import sys sys.setrecursionlimit(100000) # default is 1000 — too small for DFS on large graphs
8. Test-Driven Thinking
Write tests before code, or at minimum design your tests while you design your algorithm. This catches edge cases before they become bugs.
| Test Category | Example for "Two Sum" |
|---|---|
| Happy path | [2,7,11,15], target=9 → [0,1] — the normal case |
| Empty input | [], target=0 → [] or throw — confirm expected behavior |
| No solution | [1,2,3], target=100 → [] — answer doesn't exist |
| Duplicates | [3,3], target=6 → [0,1] — same value, different indices |
| Negative numbers | [-1,-2,3], target=1 → [1,2] |
| Large input | N=10⁵ — does it TLE? |
| Target at boundary | First two elements, last two elements |
Happy path first. Edge cases second. Performance last. Get correct before fast. Test with examples you worked by hand during the "Understand" phase — they already exist, just reuse them.
9. When You Have Time: Stress Testing
If you finish early in an online assessment, generate random tests to compare your optimized solution against the brute force. Bugs that don't appear in sample tests often appear with random inputs.
// Stress test scaffold srand(42); for (int iter = 0; iter < 1000; iter++) { int n = rand() % 10 + 1; vector<int> nums(n); for (int& x : nums) x = rand() % 20; if (brute(nums) != fast(nums)) { // print nums and break } }
Random rng = new Random(42); for (int iter = 0; iter < 1000; iter++) { int n = rng.nextInt(10) + 1; int[] nums = new int[n]; for (int i = 0; i < n; i++) nums[i] = rng.nextInt(20); if (brute(nums) != fast(nums)) { System.out.println(Arrays.toString(nums)); break; } }
import random random.seed(42) for _ in range(1000): n = random.randint(1, 10) nums = [random.randint(0, 20) for _ in range(n)] if brute(nums) != fast(nums): print("MISMATCH:", nums) break
Practice These Patterns