BugHunt

Scope and closures: variables that are not the variable you meant

Every callback reports the last value, a counter never increments, UnboundLocalError on a variable you can see.

13 free challenges5 Python8 JavaScript

What is scope and closure bug?

The name you wrote resolves to a different variable than the one you meant — a shadowed copy, a shared binding, or one that does not exist yet at the moment the line runs.

Why it happens

Closures capture variables, not values, so callbacks created in a loop can share one binding and all see the final value. Assigning to a name anywhere in a Python function makes it local for the whole function, even before the assignment. Re-declaring a name inside a callback shadows the outer one, so updates land on the copy. And in JavaScript `this` is decided by how a function is called, not where it was written.

How to recognise it

  • Every item in a loop behaves like the last one.
  • A counter is visibly incremented but stays at its initial value.
  • Python reports a variable as local when it is clearly defined above.
  • A method works when called normally and breaks when passed as a callback.

Errors and symptoms this causes

  • UnboundLocalError: cannot access local variable
  • ReferenceError: Cannot access before initialization
  • all callbacks use the last value
  • counter stays at zero
  • this is undefined

How to fix it

Do not re-declare a name you meant to update from an enclosing scope. Capture per-iteration values explicitly — a default argument in Python, let rather than var in JavaScript. Keep a method attached to its object when passing it somewhere. Declare before use, always.

Practise scope and closure bugs

Working code with one bug in it. Find it, fix it in the browser, and see the explanation. No account needed.

pythonEasy
Result variable never exists when nothing matches

last_even(nums) should return the last even number, or -1 if there are none. last_even([1, 3]) should return -1.

Scope error

pythonHard
Shopping list remembers items between calls

make_baskets(first, second) should start a separate basket for each item, so make_baskets("apple", "bread") gives [["apple"], ["bread"]].

Scope error

pythonHard
Two baskets share the same list

Each Basket should have its own items, so after adding one thing to a fresh basket, two_baskets('x', 'y') returns 1.

Scope error

javascriptHard
Every callback reports the same index

collectIndexes(n) should schedule one callback per index and resolve to the list of indexes, so collectIndexes(3) gives [0, 1, 2].

Scope error

javascriptHard
Every tracker shares one counter

Each tracker from makeTracker() should start at zero, so trackTwice(5, 3) returns 3 — the second tracker's own total.

Scope error

javascriptHard
Method loses its object when passed as a callback

countAll(items) should count how many items were passed in, so countAll([1, 2, 3]) returns 3.

Scope error

pythonHard
All the lambdas use the last factor

make_multipliers([2, 3, 4]) should build one multiplier per factor, so the FIRST one doubles: apply_first([2, 3, 4], 10) returns 20.

Scope error

javascriptHard
Hoisted var shadows the value you wanted

describe(useLocal) should return "local" when useLocal is true and the outer "global" when it is false.

Scope error

javascriptMedium
Inner count shadows the one being returned

countMatches(words, target) should count how many times target appears, so countMatches(["a","b","a"], "a") returns 2.

Scope error

javascriptMedium
Discounted price is unreachable outside the branch

finalPrice(price, isMember) should apply a 10% discount for members and return the price to pay.

Scope error

javascriptMedium
Constant used one line before it exists

priceWithFee(base) should add a flat fee of 5 to the base price, so priceWithFee(10) returns 15.

Scope error

pythonMedium
Adding to a global total raises UnboundLocalError

add_all(nums) should return the sum of the numbers, so add_all([1, 2, 3]) returns 6.

Scope error

javascriptMedium
Result variable that vanishes after the loop

findFirstNegative(nums) should return the first negative number in the array, or null if there isn't one.

Scope error

Other bug patterns