Watch the video (10 min)
On this page25 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is Scope?
  5. 2. Top-Level Scope
  6. 3. Function Scope
  7. 4. Block Scope
  8. 5. Inner Scopes See Outward
  9. 6. Shadowing
  10. 7. A Shadowing Trap
  11. 8. What Is a Closure?
  12. 9. Closures Capture Variables, Not Values
  13. 10. makeCounter() — A Function That Remembers
  14. 11. Each Closure Keeps Its Own State
  15. 12. Closures in Loops
  16. 13. A Real-World Example
  17. Code Examples
  18. Common Beginner Mistakes
  19. Best Practices
  20. Mini Challenge
  21. Practice Questions
  22. Practice Answers
  23. Quick Revision
  24. Key Takeaways
  25. What's Next?

Lecture 37 — Closures & Scope

Category6 — Functions
Lecture037 of 140
Estimated video length10 min
TargetDart 3.12.2
LevelBeginner

A local variable disappears when its function ends — so how can a function remember anything between calls? Scope decides where a name can be seen, and a closure is a function that keeps the variables it can see, even after their function has finished.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2, so you will recognize it when you meet it yourself.


Prerequisites

Students should already understand, from Lectures 001–036:

  • local and top-level variables (Lecture 030)
  • parameters and return values (Lecture 031)
  • arrow and anonymous functions (Lecture 034)
  • storing a function in a variable, and function types such as int Function() (Lecture 035)
  • functions that return other functions (Lecture 036)

Connection With Previous Lectures

In Lecture 030 we saw that a variable declared inside a function belongs to it alone, and promised to study scope in detail later. That rule creates a problem. Suppose we want a function that counts how many times it has been called:

dart
int countVisit() {
  var count = 0;
  count++;
  return count;
}

void main() {
  print(countVisit());
  print(countVisit());
  print(countVisit());
}
output
1
1
1

Every call creates a brand-new count, starting at 0. A top-level variable would remember the number, but then every function in the file could change it. We need a variable that survives between calls and stays private. Closures give us exactly that.


Learning Objectives

By the end of this lecture you will be able to:

  • Explain what scope means and why Dart's scope is decided by the braces you write
  • Identify top-level, function, and block scope
  • Predict which variable a name refers to when names are shadowed
  • Define a closure and explain what it captures
  • Build a makeCounter() function that remembers state between calls
  • Show that each closure keeps its own separate state
  • Predict what a closure created inside a loop captures
  • Recognize the error messages Dart produces for scope mistakes

1. What Is Scope?

The scope of a variable is the part of the program where its name can be used.

Analogy — think of a house. Something in the hallway can be seen from every room. Something inside the kitchen can be seen only in the kitchen. A person in the kitchen can still look out into the hallway, but someone in the hallway cannot see into the kitchen.

In Dart, the "rooms" are the pairs of braces { }. Scope is decided by where you write the code, not by when it runs. This is called lexical scope.


2. Top-Level Scope

A variable declared outside every function is in top-level scope. Every function in the file can see it:

dart
var schoolName = 'Green Valley School';

void printHeader() {
  print('== $schoolName ==');
}

void main() {
  printHeader();
  print('Welcome to $schoolName');
}
output
== Green Valley School ==
Welcome to Green Valley School

3. Function Scope

A variable declared inside a function body — and every parameter — is in that function's scope. It exists only while the function runs:

dart
void printTotal(int price, int quantity) {
  var total = price * quantity;
  print('Total: $total');
}

void main() {
  printTotal(250, 4);
  // print(total); // ✗ Error: Undefined name 'total'.
}
output
Total: 1000

price, quantity, and total all belong to printTotal(). main() cannot see any of them.


4. Block Scope

Every pair of braces creates its own scope — not just function bodies. A variable declared inside an if block or a loop body disappears at the closing brace:

dart
void main() {
  var score = 70;
  if (score > 50) {
    var message = 'Pass';
  }
  print(message);
}
output
Error: Undefined name 'message'.
  print(message);
        ^^^^^^^

To use message after the if, declare it before the block and assign it inside:

dart
void main() {
  var score = 70;
  var message = 'Fail';
  if (score > 50) {
    message = 'Pass';
  }
  print(message);
}
output
Pass

The loop variable in for (var i = 0; ...) follows the same rule: it exists only inside the loop.


5. Inner Scopes See Outward

Code can always see variables from the scopes around it — its own block, the blocks containing it, the function, and finally the top level:

dart
var currency = 'Rs';

void main() {
  var price = 500;
  for (var i = 1; i <= 2; i++) {
    var total = price * i;
    print('$i item(s): $currency $total');
  }
}
output
1 item(s): Rs 500
2 item(s): Rs 1000

Inside the loop, Dart finds total and i in the loop body, price in main(), and currency at the top level. Seeing works outward only — never inward.


6. Shadowing

An inner scope may declare a variable with the same name as an outer one. Inside the inner scope, the new variable hides — shadows — the outer one:

dart
var label = 'top-level';

void main() {
  var label = 'main';
  print(label);
  if (true) {
    var label = 'block';
    print(label);
  }
  print(label);
}
output
main
block
main

These are three different variables. When the block ends, its label disappears and the name refers to main()'s label again. The top-level label is never touched.


7. A Shadowing Trap

Once a block declares a name, that name means the new variable throughout the whole block — even on lines above the declaration:

dart
void main() {
  var x = 1;
  if (true) {
    print(x);
    var x = 2;
  }
}
output
Error: Local variable 'x' can't be referenced before it is declared.
    print(x);
          ^

Dart does not quietly fall back to the outer x. Shadowing is legal, but it makes code harder to read — prefer a different name.


8. What Is a Closure?

A closure is a function that uses a variable from a scope outside itself. The function "closes over" that variable — it takes the variable along with it:

dart
void main() {
  var name = 'Ali';
  var greet = () => print('Hello, $name!');
  greet();
}
output
Hello, Ali!

name is not a parameter of greet, and it is not declared inside it. greet can use it because of lexical scope: the function was written inside main(), where name exists.


9. Closures Capture Variables, Not Values

A closure does not take a snapshot of the value. It keeps a link to the variable itself, so it always sees the variable's current value:

dart
void main() {
  var name = 'Ali';
  var greet = () => print('Hello, $name!');
  greet();
  name = 'Sara';
  greet();
}
output
Hello, Ali!
Hello, Sara!

The link works both ways — a closure can also change a captured variable, as the next step shows.


10. makeCounter() — A Function That Remembers

Now we can solve the counting problem. makeCounter() creates a local count and returns a closure that changes it:

dart
int Function() makeCounter() {
  var count = 0;
  return () {
    count++;
    return count;
  };
}

void main() {
  var counter = makeCounter();
  print(counter());
  print(counter());
  print(counter());
}
output
1
2
3

makeCounter() has finished after its first and only call, yet count lives on — the returned closure still holds it. And nothing else in the program can reach count: it is both remembered and private.


11. Each Closure Keeps Its Own State

Every call to makeCounter() runs its body again, creating a new count and a new closure that captures it:

dart
int Function() makeCounter() {
  var count = 0;
  return () {
    count++;
    return count;
  };
}

void main() {
  var aliCounter = makeCounter();
  var saraCounter = makeCounter();
  print(aliCounter());
  print(aliCounter());
  print(saraCounter());
  print(aliCounter());
}
output
1
2
1
3

Ali's counter and Sara's counter never interfere — each has its own count.


12. Closures in Loops

In a for loop, Dart creates a fresh loop variable for every iteration. A closure made inside the loop captures that iteration's own i:

dart
void main() {
  void Function()? saved;
  for (var i = 0; i < 3; i++) {
    if (i == 1) {
      saved = () => print('Saved i: $i');
    }
  }
  saved!();
}
output
Saved i: 1

Compare a while loop, where the counter is one variable declared before the loop:

dart
void main() {
  void Function()? saved;
  var j = 0;
  while (j < 3) {
    if (j == 1) {
      saved = () => print('Saved j: $j');
    }
    j++;
  }
  saved!();
}
output
Saved j: 3

The closure captured the single j, and by the time it runs, the loop has moved j to 3. Same rule as step 9: closures capture variables, not values.


13. A Real-World Example

A quiz game keeps a separate score for each player. Each tracker is a closure with its own private total:

dart
void Function(int) makeScoreKeeper(String player) {
  var total = 0;
  return (int points) {
    total += points;
    print('$player: +$points (total $total)');
  };
}

void main() {
  var ali = makeScoreKeeper('Ali');
  var sara = makeScoreKeeper('Sara');

  ali(10);
  sara(15);
  ali(5);
  sara(20);
}
output
Ali: +10 (total 10)
Sara: +15 (total 15)
Ali: +5 (total 15)
Sara: +20 (total 35)

Each closure captures two variables from its own call of makeScoreKeeper(): the parameter player and the local total.


Code Examples

Example 1 — Top-Level and Local Variables

dart
var appName = 'Quiz App';

void showUser(String user) {
  var greeting = 'Hi, $user';
  print('$greeting — $appName');
}

void main() {
  showUser('Zayan');
}
output
Hi, Zayan — Quiz App

Example 2 — Block Scope in a Loop

dart
void main() {
  for (var day = 1; day <= 3; day++) {
    var minutes = day * 20;
    print('Day $day: $minutes min');
  }
}
output
Day 1: 20 min
Day 2: 40 min
Day 3: 60 min

Example 3 — Shadowing a Top-Level Variable

dart
var city = 'Lahore';

void main() {
  var city = 'Karachi';
  print('Inside main: $city');
  printCity();
}

void printCity() {
  print('Top-level: $city');
}
output
Inside main: Karachi
Top-level: Lahore

printCity() is written outside main(), so it sees only the top-level city.

Example 4 — A Closure Sees Changes

dart
void main() {
  var count = 5;
  var show = () => print('Count is $count');
  show();
  count = 8;
  show();
}
output
Count is 5
Count is 8

Example 5 — A Counter

dart
int Function() makeCounter() {
  var count = 0;
  return () {
    count++;
    return count;
  };
}

void main() {
  var next = makeCounter();
  print(next());
  print(next());
}
output
1
2

Example 6 — Capturing a Parameter

dart
int Function(int) makeAdder(int amount) {
  return (int value) => value + amount;
}

void main() {
  var addTen = makeAdder(10);
  var addHundred = makeAdder(100);
  print(addTen(5));
  print(addHundred(5));
}
output
15
105

A parameter is a local variable too, so a closure can capture it.

Example 7 — Student Marks

dart
bool Function(int) makePassChecker(int passMark) {
  return (int marks) => marks >= passMark;
}

void main() {
  var schoolTest = makePassChecker(40);
  var scholarship = makePassChecker(85);
  print('Ali (72) passes test: ${schoolTest(72)}');
  print('Ali (72) gets scholarship: ${scholarship(72)}');
}
output
Ali (72) passes test: true
Ali (72) gets scholarship: false

Example 8 — Shopping Total

dart
int Function(int) makeCart() {
  var total = 0;
  return (int price) {
    total += price;
    return total;
  };
}

void main() {
  var cart = makeCart();
  cart(450);
  cart(1200);
  print('Total: ${cart(300)}');
}
output
Total: 1950

Every call adds to the same private total: 450 + 1200 + 300 = 1950.

Example 9 — Bank Balance

dart
double Function(double) makeAccount(double opening) {
  var balance = opening;
  return (double amount) {
    balance += amount;
    return balance;
  };
}

void main() {
  var sara = makeAccount(1000);
  print(sara(500));
  print(sara(-200));
}
output
1500.0
1300.0

A positive amount is a deposit and a negative one a withdrawal. balance can only change through the closure.

Example 10 — Highest Temperature

dart
double Function(double) makeHighTracker() {
  var highest = -100.0;
  return (double reading) {
    if (reading > highest) {
      highest = reading;
    }
    return highest;
  };
}

void main() {
  var track = makeHighTracker();
  print(track(31.5));
  print(track(36.0));
  print(track(29.0));
}
output
31.5
36.0
36.0

The closure remembers the highest reading so far; a lower reading leaves it unchanged.


Common Beginner Mistakes

1. Using a block variable after its block

dart
void main() {
  var marks = 80;
  if (marks > 50) {
    var grade = 'A';
  }
  // print(grade); // ✗
}
output
Error: Undefined name 'grade'.

Declare grade before the if and assign it inside.

2. Reading a name before a shadowing declaration

dart
void main() {
  var total = 100;
  if (total > 50) {
    // print(total); // ✗
    var total = 200;
    print(total);
  }
}
output
Error: Local variable 'total' can't be referenced before it is declared.

Inside that block, total already means the new variable. Use a different name.

3. Shadowing by accident

dart
var total = 0;

void addSale(int amount) {
  var total = 0; // ✗ a new local variable, not the top-level one
  total += amount;
}

void main() {
  addSale(500);
  print(total);
}
output
0

var created a new local total, so the top-level one never changed. Drop the var to use the outer variable.

4. Calling makeCounter() every time

dart
int Function() makeCounter() {
  var count = 0;
  return () {
    count++;
    return count;
  };
}

void main() {
  print(makeCounter()()); // ✗ a new counter each time
  print(makeCounter()());
}
output
1
1

Each makeCounter() call creates a fresh count. Create the counter once, store it, and call the stored closure.


Best Practices

  • Declare variables in the smallest scope that needs them.
  • Avoid shadowing; choose a new, descriptive name instead.
  • Prefer a closure's private state over a top-level variable that every function can change.
  • Create a counter or tracker once, store it in a variable, then call it.

Mini Challenge

Write makeStepCounter(int step) that returns a closure. Each call adds step to a private total and returns it. Create a counter with a step of 5 and call it three times.

Your program should print exactly:

output
5
10
15

Try it yourself before opening the solution.

Show solution
dart
int Function() makeStepCounter(int step) {
  var total = 0;
  return () {
    total += step;
    return total;
  };
}

void main() {
  var byFive = makeStepCounter(5);
  print(byFive());
  print(byFive());
  print(byFive());
}
output
5
10
15

The closure captures both the parameter step and the local total.


Practice Questions

Work through these in order — they run from beginner to application to challenge. Every question gives you exact values, so you can compare your output with the answers.

Beginner

  1. Predict the output, then run it to check:

    dart
    var color = 'red';
    
    void main() {
      var color = 'blue';
      print(color);
    }
  2. This program does not compile. Fix it so that it prints Adult, without changing the print line:

    dart
    void main() {
      var age = 20;
      if (age >= 18) {
        var status = 'Adult';
      }
      print(status);
    }
  3. In main(), store var item = 'Pen'; and a closure show that prints Item: $item. Call show, change item to 'Book', and call show again.

Application

  1. Write makeCountdown() that returns an int Function(). The closure starts from 3 and returns 3, then 2, then 1 on successive calls.

  2. Using makeCounter() from step 10, create ali and zayan counters. Call ali twice and zayan once, printing each result. What does the program print?

  3. Predict the output, then run it:

    dart
    void main() {
      void Function()? saved;
      for (var i = 1; i <= 3; i++) {
        if (i == 2) {
          saved = () => print(i);
        }
      }
      saved!();
    }
  4. Write makeMultiplier(int factor) that returns an int Function(int). Use it to create twice and thrice, then print twice(7) and thrice(7).

Challenge

  1. Write makePiggyBank(int start) for Zayan. The closure takes an amount, adds it, and returns the new balance. Start at 150, add 50, then add -30, and print 170.
  2. Write makeLimitedCounter(int max) whose closure counts 1, 2, 3, … but never goes past max. With max = 3, four calls print 1, 2, 3, 3.
  3. Write makeAverager() whose closure takes a mark and returns the average of all marks given so far as a double. Feed it 80, 90, and 70 and print each result.

Practice Answers

Question 1 — Solution

output
blue

The local color in main() shadows the top-level one.

Question 2 — Solution

dart
void main() {
  var age = 20;
  var status = 'Minor';
  if (age >= 18) {
    status = 'Adult';
  }
  print(status);
}
output
Adult

Question 3 — Solution

dart
void main() {
  var item = 'Pen';
  var show = () => print('Item: $item');
  show();
  item = 'Book';
  show();
}
output
Item: Pen
Item: Book

Question 4 — Solution

dart
int Function() makeCountdown() {
  var current = 4;
  return () {
    current--;
    return current;
  };
}

void main() {
  var countdown = makeCountdown();
  print(countdown());
  print(countdown());
  print(countdown());
}
output
3
2
1

Question 5 — Solution

dart
int Function() makeCounter() {
  var count = 0;
  return () {
    count++;
    return count;
  };
}

void main() {
  var ali = makeCounter();
  var zayan = makeCounter();
  print(ali());
  print(ali());
  print(zayan());
}
output
1
2
1

Question 6 — Solution

output
2

A for loop creates a fresh i for each iteration, so the closure keeps the i from the iteration where i was 2.

Question 7 — Solution

dart
int Function(int) makeMultiplier(int factor) {
  return (int value) => value * factor;
}

void main() {
  var twice = makeMultiplier(2);
  var thrice = makeMultiplier(3);
  print(twice(7));
  print(thrice(7));
}
output
14
21

Question 8 — Solution

dart
int Function(int) makePiggyBank(int start) {
  var balance = start;
  return (int amount) {
    balance += amount;
    return balance;
  };
}

void main() {
  var zayan = makePiggyBank(150);
  zayan(50);
  print(zayan(-30));
}
output
170

Question 9 — Solution

dart
int Function() makeLimitedCounter(int max) {
  var count = 0;
  return () {
    if (count < max) {
      count++;
    }
    return count;
  };
}

void main() {
  var counter = makeLimitedCounter(3);
  print(counter());
  print(counter());
  print(counter());
  print(counter());
}
output
1
2
3
3

Question 10 — Solution

dart
double Function(int) makeAverager() {
  var sum = 0;
  var count = 0;
  return (int mark) {
    sum += mark;
    count++;
    return sum / count;
  };
}

void main() {
  var average = makeAverager();
  print(average(80));
  print(average(90));
  print(average(70));
}
output
80.0
85.0
80.0

The closure keeps two private variables. / always gives a double.


Quick Revision

  1. What is the scope of a variable?
  2. Why is Dart's scope called lexical?
  3. Name the three kinds of scope from this lecture.
  4. Can code inside a block see a variable declared in main()? Can main() see a variable declared inside the block?
  5. What is shadowing, and why is it best avoided?
  6. What does a closure capture — a variable or a value?
  7. Why do two calls to makeCounter() give two independent counters?
  8. In a for loop, does each iteration get its own loop variable?

Key Takeaways

  • Scope is where a name can be used; Dart decides it from the braces you write (lexical scope).
  • Top-level variables are visible everywhere; function variables and parameters only inside their function; block variables only inside their braces.
  • Inner scopes see outward, never inward.
  • A variable with the same name in an inner scope shadows the outer one — avoid it.
  • A closure is a function that uses variables from an outer scope and keeps them alive.
  • Closures capture variables, not values, so they see — and can change — the current value.
  • Each call to a function like makeCounter() creates new variables, so each closure has its own private state.
  • A for loop gives every iteration a fresh loop variable; a while loop's counter is one shared variable.

What's Next?

Lecture 038 — Recursion & Function Design. We have now seen functions call other functions, take functions, return functions, and remember state. One question is left: what happens when a function calls itself? Next we study recursion, and finish the category with practical rules for designing clear, well-shaped functions.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.