MediumAsync & Effects🐛 Debug Challenge

State update after component unmount

Buggy Code — Can you spot the issue?

const log = [];
let mounted = true;

function setData(value) {
  if (!mounted) {
    log.push('ERROR: setState after unmount');
    return;
  }
  log.push('setState: ' + value);
}

// Simulates useEffect that fetches without cleanup
async function simulateEffect() {
  const data = await Promise.resolve('user data');
  setData(data); // might be called after unmount!
}

simulateEffect(); // start fetch

// Component unmounts before fetch completes
mounted = false;

// But the async code still runs and calls setState
setTimeout(() => console.log(log.join(' | ')), 0);

Fixed Code

const log = [];
let mounted = true;

function setData(value) {
  if (!mounted) {
    log.push('ERROR: setState after unmount');
    return;
  }
  log.push('setState: ' + value);
}

// Fix: cancelled flag — useEffect cleanup sets it to true
async function simulateEffect() {
  let cancelled = false;
  const cleanup = () => { cancelled = true; };

  const data = await Promise.resolve('user data');

  if (!cancelled) {
    setData(data);  // only update if still mounted
  } else {
    log.push('fetch cancelled — component unmounted');
  }

  return cleanup;
}

const getCleanup = simulateEffect();

// Component unmounts
mounted = false;
getCleanup.then(cleanup => cleanup());

setTimeout(() => console.log(log.join(' | ')), 0);

Bug Explained

Bug: The async fetch resolves after unmount. setState is called on an unmounted component — memory leak and potential errors.

Explanation: The cleanup function sets cancelled = true. When the fetch resolves, it checks cancelled before calling setState.

Key Insight: Pattern for async useEffect: let cancelled = false; return () => { cancelled = true; }. Check !cancelled before every setState in the async callback.

Practice spotting bugs live →

38 debug challenges with AI hints

🐛 Try Debug Lab