HardStale Closures🐛 Debug Challenge

useCallback stale dep — handler misses latest value

Buggy Code — Can you spot the issue?

let formValue = '';
const log = [];

// Simulates useCallback(() => validate(formValue), [])
// Bug: empty deps — formValue never updates in handler
function createSubmitHandler() {
  const capturedValue = formValue; // '' at creation
  return () => {
    if (!capturedValue) {
      log.push('invalid: empty');
    } else {
      log.push('valid: ' + capturedValue);
    }
  };
}

const handleSubmit = createSubmitHandler(); // created once, [] deps

formValue = 'Alice'; // user types
handleSubmit();       // still reads ''

formValue = 'Bob';
handleSubmit();

Fixed Code

let formValue = '';
const log = [];

// Fix: include formValue in deps by recreating handler when it changes
function createSubmitHandler(currentValue) {
  return () => {
    if (!currentValue) {
      log.push('invalid: empty');
    } else {
      log.push('valid: ' + currentValue);
    }
  };
}

formValue = 'Alice';
let handleSubmit = createSubmitHandler(formValue); // recreated with new value
handleSubmit();

formValue = 'Bob';
handleSubmit = createSubmitHandler(formValue);
handleSubmit();

Bug Explained

Bug: capturedValue is '' because createSubmitHandler was called before formValue was updated. Empty deps ([]) means the handler is never recreated.

Explanation: Recreating the handler with the current value as a parameter breaks the stale closure. In React, this means adding formValue to useCallback's dep array.

Key Insight: useCallback with empty deps creates a permanent stale closure. Add all referenced state/props to deps. The linter rule exhaustive-deps catches this automatically.

Practice spotting bugs live →

38 debug challenges with AI hints

🐛 Try Debug Lab