Dart (3.13) Module 3 – Control flow and functions
Closures, function types and tear-offs
Pass Dart functions as values, name them with function types and typedefs, build closures that capture state, and use tear-offs including constructor tear-offs.
What you will learn
- Pass functions as values and write function types and typedefs
- Create closures that capture state and avoid capture surprises
- Use tear-offs, including constructor tear-offs
- Use higher-order functions that take or return functions
Before you start
What you’ll learn · Before you start
What you will learn
- Pass functions as values and write function types and typedefs
- Create closures that capture state and avoid capture surprises
- Use tear-offs, including constructor tear-offs
- Use higher-order functions that take or return functions
Before you start
On this page
In Dart a function is an object you can store in a variable, pass to another function and return from one. That one
fact is behind callbacks, the map and where you use on lists, and closures that remember state between calls. It
also raises a question this lesson answers: when a function captures a variable, what exactly does it hold?
You will store functions in a typed list, name a function type with a typedef, build a counter factory whose two
counters do not interfere, tear off methods and constructors to pass by name, and see why the bare Function type
gives up the checking a precise function type keeps.
Functions are values
A function has a type, written returnType Function(argTypes), and a value of that type can be stored and passed like
any other. validators.dart names such a type with a typedef: A named alias for a type, most often a function type, such as typedef Validator = String? Function(String). It documents intent and keeps declarations readable; it creates no new class. and keeps a list of them:
// A typedef names a function type. Each validator takes a value and returns an
// error message, or null when the value is fine.
typedef Validator = String? Function(String value);
String? firstError(String value, List<Validator> rules) {
for (final rule in rules) {
final error = rule(value);
if (error != null) return error;
}
return null;
}
void main() {
final rules = <Validator>[
(v) => v.isEmpty ? 'required' : null,
(v) => v.length < 10 ? 'too short' : null,
(v) => v.contains(RegExp(r'[^0-9]')) ? 'digits only' : null,
];
print(firstError('', rules));
print(firstError('123', rules));
print(firstError('98x6543210', rules));
print(firstError('9876543210', rules));
} Output
required too short digits only null
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run validators.dart
typedef Validator = String? Function(String value) gives the function type: The type of a function value, written return-type Function(argument-types), such as int Function(int). The bare type Function accepts any function but loses argument checking. a name, so
List<Validator> reads clearly and every rule in the list is checked against it. The output was required, then
too short, then digits only, then null: firstError ran each rule in turn and returned the first non-null
message, or null when the value passed them all. Prefer a precise function type like this over the bare Function
type, which accepts any function and, as the last section shows, drops the compiler’s checks.
Storing a function in a variable works the same way as storing a number. You can write final parse = int.parse; and
call parse('42') later, assign a different function to a var of a function type, keep functions in a Map to pick
one by key, or return one from another function. Because the type is inferred, var greet = (String n) => 'Hi $n';
gives greet the type String Function(String) without your writing it. This is what “first-class functions” means:
a function is an ordinary value, with a type, that goes wherever a value can go.
Checkpoint
Check: Functions are values
Closures capture state
A closure: A function that refers to variables from the scope where it was created and keeps them alive. It captures the variable itself, not a copy, so it sees and can change later values. is a function that refers to variables from the scope where it was made, and it
captures the variable, not a snapshot. counter.dart returns a closure over a local count:
// makeCounter returns a closure that captures its own count variable. Each
// call to makeCounter makes a fresh count, so the counters are independent.
int Function() makeCounter() {
var count = 0;
return () => ++count;
}
void main() {
final a = makeCounter();
final b = makeCounter();
print(a());
print(a());
print(a());
print(b());
} Output
1 2 3 1
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run counter.dart
It printed 1, 2, 3, 1. Each makeCounter() call creates a fresh count, and the returned closure keeps
reading and incrementing that one variable, so the count survives between calls even though the function that declared
it has returned. Because a and b come from two separate calls, they hold two separate count variables and never
interfere. Step through why:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- The first counter. makeCounter creates a count set to 0 and returns a closure that reads and increments it. a holds that closure, and the count lives on as long as a does.
- Calling a three times. Each call runs ++count on a's own count, so a() returned 1, then 2, then 3. The closure changes the variable it captured, not a copy.
- The second counter. A second makeCounter call makes a brand-new count at 0, captured by b. It is a different variable, so a's count stays at 3.
- b is independent. b() increments b's own count to 1 and returns 1, while a's counter is untouched. One factory, two separate pieces of state, which is why the program printed 1, 2, 3, 1.
This is the same capture rule the loops lesson showed with [0, 1, 2], seen from the other side: there each turn gave
a new variable, so the closures differed; here each factory call gives a new variable, so the counters differ, while
repeated calls to one counter share and change the same one.
Key idea
A closure captures the variable, not its value at the time. So a counter’s closure keeps changing one count, while
two counters from separate calls hold separate variables that never touch.
Checkpoint
Check: Closures capture state
Tear-offs pass a function by name
When you already have a function and only want to pass it, you do not need to wrap it in (x) => f(x). Writing its
name alone is a tear-off: Using a function's name as a value without calling it, such as map(int.parse) or map(User.new). Dart tears off methods, top-level functions and, since 2.15, constructors.. tearoffs.dart tears off a static method, a constructor and a
top-level function:
// A tear-off passes a function by name without calling it. int.parse, the
// constructor User.new and the top-level print are all torn off here.
class User {
final String name;
User(this.name);
@override
String toString() => 'User($name)';
}
void main() {
final nums = ['1', '2', '3'].map(int.parse).toList();
print(nums);
final users = ['Asha', 'Ravi'].map(User.new).toList();
print(users);
['a', 'b'].forEach(print);
} Output
[1, 2, 3] [User(Asha), User(Ravi)] a b
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run tearoffs.dart
['1', '2', '3'].map(int.parse) printed [1, 2, 3]: int.parse is handed to map as a value and called once per
element. User.new is a constructor tear-off, so ['Asha', 'Ravi'].map(User.new) built [User(Asha), User(Ravi)],
and forEach(print) passed the top-level print straight through, printing a and b. A tear-off keeps the code
shorter and reads as “apply this function”, and it is exactly the function, not a new wrapper closure. The analyzer
even suggests it: the unnecessary_lambdas lint flags (x) => f(x) and offers to replace it with the tear-off f.
Constructor tear-offs since 2.15
User.new as a value arrived in Dart 2.15; before it you wrote (name) => User(name). Named constructors tear off
too, as User.guest.
Checkpoint
Check: Tear-offs
Functions that take and return functions
A higher-order function: A function that takes a function as an argument or returns one, such as map and where, or a factory like multiplier(by) that returns a closure capturing by. takes a function as an argument or returns one. higherorder.dart does both:
// A higher-order function takes or returns a function. multiplier returns a
// closure; where and fold take one.
int Function(int) multiplier(int by) => (n) => n * by;
void main() {
final triple = multiplier(3);
print(triple(5));
final nums = [1, 2, 3, 4, 5, 6];
final evens = nums.where((n) => n.isEven).toList();
print(evens);
final sum = nums.fold<int>(0, (a, b) => a + b);
print(sum);
} Output
15 [2, 4, 6] 21
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run higherorder.dart
multiplier(3) returned a closure that multiplies by 3, so triple(5) printed 15; the captured by is what makes
each returned function different. where((n) => n.isEven) kept the even numbers, printing [2, 4, 6], and
fold(0, (a, b) => a + b) combined the list into 21. These three, map, where and fold, take a function and
apply it across a collection, which is why passing functions as values matters in everyday Dart. An object can even be
made callable by giving it a call method, so it can be used in a function’s place; the callable-objects page covers
that.
map and where return a lazy Iterable, not a list: the function you pass runs only as the result is read, which is
why the examples call .toList() to force the work and print a list. fold is different; it walks the whole
collection at once and returns a single combined value, carrying a running result from one element to the next. Knowing
which methods are lazy and which are eager stops a common surprise, a map whose function seems never to run because
nothing has read its result yet.
Finally, precision matters when you type a function value. bare_function.dart stores a closure in the bare Function
type:
// The bare Function type accepts any function and loses argument checking, so
// a wrong call compiles and fails only at run time.
void main() {
Function f = (int x) => x + 1;
print(f(3));
print(f('oops'));
} Output (exit status 255)
4
Printed as an error (standard error)
Unhandled exception: type 'String' is not a subtype of type 'int' of 'x' #0 _TypeError._throwNew (dart:core-patch/errors_patch.dart:108:4) #1 main (bare_function.dart:6:10) #2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:313:19) #3 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run bare_function.dart
f(3) printed 4, but f('oops') compiled and then threw type 'String' is not a subtype of type 'int' at run time,
ending with status 255. The bare Function type accepts any function and defers argument checks to run time; a precise
type such as int Function(int) would have rejected f('oops') before the program ran.
Checkpoint
Check: Higher-order functions
Practise: validator combinators
Build small functions that return validators, reusing the Validator typedef from this lesson.
Validator maxLength(int n)returns a validator that reports'too long'when a value is longer thann, andnullotherwise.Validator matches(RegExp re, String message)returnsmessagewhen the value does not matchre, andnullwhen it does.Validator all(List<Validator> vs)returns a validator that runs each one in order and returns the first error, ornullwhen every one passes.- Apply
all([...])to a few inputs and confirm the error order is the order of the list, that an empty list always returnsnull, and that a valid value returnsnull.
When the combinators behave, the self-check revisits every section:
Practice that adapts to you
Practise this lesson
5 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What does a Dart closure capture, the value of a variable or the variable itself?
The variable itself, not a copy of its value at the moment the closure was made. That is why a counter closure can
keep incrementing one count across calls: it holds the variable, so each call sees the latest value and can change
it. It is also why two closures made from separate makeCounter calls are independent, since each call introduced its
own count. The same rule explains the loops lesson’s result: each turn of a for loop binds a fresh variable, so
closures made on different turns capture different variables and read different values. When you want a closure to keep
a fixed snapshot of a value that will go on changing, copy that value into a new local inside the scope and capture the
local instead. Capture is about variables, and the number of distinct variables is what decides what each closure sees.
To go further, the bank has two questions that dig into capture surprises and tear-offs:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
In a review you see both items.map((s) => int.parse(s)) and items.map(int.parse). A colleague says they are identical and the choice is pure style. Are they always interchangeable? Explain when a tear-off and a wrapping lambda differ, with examples.
Flashcards · 5 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What is the type of a Dart function that takes an A and returns an R?
R Function(A). A function is a value of that type: store it, pass it or return it, and name the type with a typedef such as Validator.
-
Does a closure capture a variable or its value?
The variable. A counter's closure keeps changing one count, while two counters from separate factory calls hold separate variables that never touch.
-
What is a tear-off?
A function passed by its name without calling it, such as int.parse, the top-level print or a constructor: User.new, or a named one such as User.guest. Constructor tear-offs arrived in Dart 2.15.
-
What is a higher-order function?
One that takes or returns a function. map, where and fold take one, and a factory like multiplier returns one that captures its argument.
-
Why prefer a precise function type over the bare Function?
Function accepts any function and leaves the argument checks until the call runs, so f('oops') threw at run time instead of failing to compile.
Key takeaways
- A function is a value with a type
R Function(A); store it, pass it, return it, and name the type with atypedefsuch asValidator. - A closure captures the variable, not its value, so a counter keeps changing one
count, while counters from separate factory calls hold separate variables. - A tear-off passes a function by its name, including static methods (
int.parse), the top-levelprintand constructors (User.new, since Dart 2.15). - Higher-order functions take or return functions;
map,whereandfoldtake one, and a factory likemultiplierreturns one that captures its argument. - Prefer a precise function type over the bare
Function, which accepts any function and defers argument checks to run time, wheref('oops')threw instead of failing to compile.
Terms in this lesson
- closure
- A function that refers to variables from the scope where it was created and keeps them alive. It captures the variable itself, not a copy, so it sees and can change later values.
- function type
- The type of a function value, written return-type Function(argument-types), such as int Function(int). The bare type Function accepts any function but loses argument checking.
- higher-order function
- A function that takes a function as an argument or returns one, such as map and where, or a factory like multiplier(by) that returns a closure capturing by.
- tear-off
- Using a function's name as a value without calling it, such as map(int.parse) or map(User.new). Dart tears off methods, top-level functions and, since 2.15, constructors.
- typedef
- A named alias for a type, most often a function type, such as typedef Validator = String? Function(String). It documents intent and keeps declarations readable; it creates no new class.
References
- Functions (The Dart project (Google))
- Typedefs (The Dart project (Google))
- Callable objects (The Dart project (Google))
Report a problem with this lesson
Kept only in this browser. Your Learn progress