Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 5 minutes
  • Examples run with dart 3.13.5 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 5 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

validators.dart: a typedef and a list of function values Dart · validators.dart
// 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

  1. Question 1 of 2 What is the type of a function that takes an int and returns a String?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: String Function(int)

    A function value has a type like String Function(int), which is what a typedef such as Validator names.

  2. Question 2 of 2 What does typedef Validator = String? Function(String value); give you?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A name for a function type, usable as Validator anywhere

    validators.dart used the Validator typedef to type a List<Validator> of rules.

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:

counter.dart: a factory whose counters keep their own state Dart · counter.dart
// 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:

Interactive · step by step Two counters, each with its own captured count Step through counter.dart. Each makeCounter call captures a fresh count, so the two counters never share state.
Two counters, each with its own captured count, step 4 of 4. Showing a = makeCounter(), count (a) = 0 (count (a) = 3), b = makeCounter(), count (b) = 0 (count (b) = 1). In focus: count (b) = 0.a = makeCounter()count (a) = 0count (a) = 3b = makeCounter()count (b) = 0count (b) = 1

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. One question makeCounter returns () => ++count over a local count. Why do two counters not interfere?

    Choose one answer.

    Show the answer: One question

    Answer: Each makeCounter call creates its own count variable

    counter.dart printed 1, 2, 3, 1 because a and b captured separate count variables.

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:

tearoffs.dart: tearing off int.parse, User.new and print Dart · tearoffs.dart
// 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

  1. One question What does writing int.parse as an argument, as in map(int.parse), do?

    Choose one answer.

    Show the answer: One question

    Answer: It passes the int.parse function itself, to be called per element

    tearoffs.dart tore off int.parse and User.new, so map called each once per element.

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:

higherorder.dart: a factory, then where and fold Dart · higherorder.dart
// 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:

bare_function.dart: the bare Function type loses checking Dart · bare_function.dart
// 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

  1. Question 1 of 2 What makes multiplier a higher-order function?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It returns a function

    multiplier(3) returned a closure, so triple(5) printed 15; where and fold take functions instead.

  2. Question 2 of 2 When is a wrong call to a value of the bare Function type caught?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: At run time, as a TypeError

    bare_function.dart printed 4, then f('oops') threw type 'String' is not a subtype of type 'int' at run time.

Practise: validator combinators

Build small functions that return validators, reusing the Validator typedef from this lesson.

  1. Validator maxLength(int n) returns a validator that reports 'too long' when a value is longer than n, and null otherwise.
  2. Validator matches(RegExp re, String message) returns message when the value does not match re, and null when it does.
  3. Validator all(List<Validator> vs) returns a validator that runs each one in order and returns the first error, or null when every one passes.
  4. Apply all([...]) to a few inputs and confirm the error order is the order of the list, that an empty list always returns null, and that a valid value returns null.

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.

  1. Question 1 of 5 A closure captures a variable. What does it hold?

    Choose one answer.

    Show the answer: Question 1 of 5

    Answer: The variable itself, so it sees later changes

    The counter kept changing one count because the closure held the variable, not a copy.

  2. Question 2 of 5 You need ten independent counters. How do you make them?

    Choose one answer.

    Show the answer: Question 2 of 5

    Answer: Call makeCounter ten times, so each gets its own captured count

    Independence comes from separate variables, which separate makeCounter calls provide.

  3. Question 3 of 5 What is names.map(User.new) doing?

    Choose one answer.

    Show the answer: Question 3 of 5

    Answer: Tearing off the User constructor and calling it once per name

    tearoffs.dart built [User(Asha), User(Ravi)] from ['Asha', 'Ravi'].map(User.new).

  4. Question 4 of 5 triple = multiplier(3); what does the returned closure capture, and what does triple(5) give?

    Choose one answer.

    Show the answer: Question 4 of 5

    Answer: It captures by = 3, so triple(5) returns 15

    higherorder.dart printed 15, because the closure multiplies its argument by the captured by of 3.

  5. Question 5 of 5 You add (v) => v.length to the <Validator> list, where Validator is String? Function(String). What happens?

    Choose one answer.

    Show the answer: Question 5 of 5

    Answer: It does not compile, because the lambda returns int where String? is expected

    validators.dart kept every rule as String? Function(String), so each lambda must return a message or null. A typedef is a checked contract, not documentation.

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.

  1. Mid to senior

    A developer builds a list of callbacks in a loop and, inside each, reads a counter they declared once above the loop and increment as they go. Every callback ends up reporting the same final count, which is not what they wanted. Explain what each callback captured, and how to give each its own value.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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 a typedef such as Validator.
  • 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-level print and constructors (User.new, since Dart 2.15).
  • Higher-order functions take or return functions; map, where and fold take one, and a factory like multiplier returns 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, where f('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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.