TryBugHunt

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.

26 free challenges14 Python12 JavaScript

What are scope and closure bugs?

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.

Error guides caused by this pattern

All error guides →

In-depth guides for common exceptions and runtime errors triggered by scope and closure bugs, with working fixes and real-world examples:

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.

Breaks vs Works Example

Breaks

# Python: closures capture variable binding, not iteration value
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])  # [2, 2, 2]

Works

# Python: default argument binds variable at function definition
funcs = [lambda x=i: x for i in range(3)]
print([f() for f in funcs])  # [0, 1, 2]

💡 Closures capture bindings. In JS, use 'let' instead of 'var' so each loop iteration gets a fresh binding.

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
Tip calculator crashes with NameError

total_with_tip(bill, tip_percent) adds a tip to a bill. Example: total_with_tip(100, 10) → 110.

Scope error

pythonEasy
Calculating bonus raises UnboundLocalError when condition is false

calculate_bonus(score, is_vip) adds a 50 bonus if is_vip is True, adding 0 otherwise. Example: calculate_bonus(100, False) → 100.

Scope error

pythonEasy
Last even number crashes when there is none

last_even(nums) returns the last even number, or -1 if there are none. Example: last_even([1, 2, 3, 4]) → 4, last_even([1, 3]) → -1.

Scope error

pythonHard
Two shoppers end up with the same basket

make_baskets(first, second) puts each item in its own new basket. Example: make_baskets("apple", "bread") → [["apple"], ["bread"]].

Scope error

pythonHard
New basket already has the first basket's item

Each Basket should have its own list of items. two_baskets(first, second) adds one item to each of two baskets and returns how many items the second has. Example: two_baskets("x", "y") → 1.

Scope error

javascriptHard
Every callback reports index 3

collectIndexes(n) starts one delayed callback for each index and returns the indexes they report. Example: collectIndexes(3) → [0, 1, 2].

Scope error

javascriptHard
New tracker starts with the old tracker's total

Each tracker from makeTracker() starts at 0. trackTwice(a, b) adds a to one tracker and b to a new one, and returns the second total. Example: trackTwice(5, 3) → 3.

Scope error

javascriptHard
Counter method crashes inside forEach

countAll(items) counts the items using a Counter object. Example: countAll([1, 2, 3]) → 3.

Scope error

pythonHard
Three multipliers all multiply by 2

make_multipliers() returns 3 functions that multiply by 0, 1 and 2. test_multipliers() calls each with 10. Example: test_multipliers() → [0, 10, 20].

Scope error

pythonHard
All multipliers use the last number

make_multipliers(factors) makes one multiplier function per factor. apply_first uses the first one. Example: apply_first([2, 3, 4], 10) → 20.

Scope error

javascriptHard
describe(false) can't see the global label

describe(useLocal) returns "local" when useLocal is true, and the outer label "global" when it is false. Example: describe(false) → "global".

Scope error

javascriptMedium
Counting matches always gives 0

countMatches(words, target) counts how many times target appears. Example: countMatches(["a", "b", "a"], "a") → 2.

Scope error

pythonMedium
Second list still has the first call's item

append_to_list(val) puts val in a new list when no list is given. test_append() calls it with 1, then with 2. Example: test_append() → [[1], [2]].

Scope error

javascriptMedium
Creating grid with array fill shares same row object reference

update_grid_cell(rows, cols, r, c, val) creates a 2D zero-filled grid of size rows × cols and updates only the cell at row r, column c. Example: update_grid_cell(2, 2, 0, 0, 99) → [[99, 0], [0, 0]].

Scope error

pythonMedium
Label maker says str is not callable

make_label(text, number) builds an order label. Example: make_label("Order", 5) → "Order #5".

Scope error

pythonMedium
Class variable list leaks items across multiple user carts

track_two_user_carts(item1, item2) creates two independent carts and adds item1 to the first and item2 to the second. Example: track_two_user_carts("apple", "banana") → [["apple"], ["banana"]].

Scope error

javascriptMedium
Order filter with global regex drops every second match

filter_valid_orders(orderIds) filters an array of order IDs and returns only those matching the pattern "ORD-XXXX". Example: filter_valid_orders(["ORD-1001", "ORD-1002"]) → ["ORD-1001", "ORD-1002"].

Scope error

pythonMedium
Multiplying row list in matrix creates identical row references

update_matrix_value(rows, cols, r, c, val) creates a 2D zero-filled grid and updates only the cell at row r, column c. Example: update_matrix_value(2, 2, 0, 0, 99) → [[99, 0], [0, 0]].

Scope error

pythonMedium
Click counter crashes on the first click

count_clicks(n) clicks a Counter n times and returns the count. Example: count_clicks(3) → 3.

Scope error

javascriptMedium
Member discount crashes with ReferenceError

finalPrice(price, isMember) gives members 10% off and returns the price to pay. Example: finalPrice(100, true) → 90, finalPrice(100, false) → 100.

Scope error

javascriptMedium
Adding a fee crashes before the fee exists

priceWithFee(base) adds a fee of 5 to the price. Example: priceWithFee(10) → 15.

Scope error

javascriptMedium
Array fill with empty array shares same sublist instance

initialize_buckets(count, firstItem) creates count independent buckets and adds firstItem to bucket 0 only. Example: initialize_buckets(2, 'a') → [['a'], []].

Scope error

pythonMedium
Adding to total crashes with UnboundLocalError

add_all(nums) returns the sum of the numbers. Example: add_all([1, 2, 3]) → 6.

Scope error

javascriptMedium
Object freeze is shallow and allows nested settings mutation

protect_nested_config(config, newPort) prevents mutating nested configuration properties on config. Example: protect_nested_config({ database: { port: 5432 } }, 9999) → 5432.

Scope error

javascriptMedium
Finding the first negative number crashes

findFirstNegative(nums) returns the first negative number in the array, or null if there is none. Example: findFirstNegative([3, -2, 5]) → -2.

Scope error

pythonMedium
Mutable default context dictionary accumulates keys across calls

build_two_contexts(id1, id2) creates two independent session contexts with their respective user IDs. Example: build_two_contexts(10, 20) → [10, 20].

Scope error

Stuck on your own code?

Paste it into the visualizer and watch it run line by line, with every variable at every step. Free, and it runs in your browser.

Other bug patterns