Functions and closures
Dart has three parameter forms, and mixing them up is the most common syntax mistake:
- Positional (required):
void log(String msg, int level) {}— passed in order. - Optional positional — square brackets:
void log(String msg, [int level = 0]) {}— can be omitted and given a default. - Named — curly braces:
void log({required String msg, int level = 0}) {}— named at the call site:log(msg: 'hi').
Rules: [] and {} cannot be combined in the same signature; since null safety a non-nullable named parameter with no default must be required; a default value must always be a compile-time constant.
| Form | Signature | Call site | When to use |
|---|---|---|---|
| Positional | `f(int a, int b)` | `f(1, 2)` | 1-2 obvious arguments |
| Optional positional | `f(int a, [int b = 0])` | `f(1)` or `f(1, 2)` | Rare; mostly legacy APIs |
| Named | `f({required int a, int b = 0})` | `f(a: 1, b: 2)` | 3+ arguments, boolean flags, config objects |
Functions are first-class values. You can assign a function to a variable, pass it as a parameter and return it. There are two ways to write a function type:
- Inline:
void run(int Function(String) parse) {} - Named via
typedef:typedef Parser = int Function(String raw);
A typedef makes long callback signatures readable and keeps a repeated signature in one place. Anonymous functions ((x) => x * 2) and arrow syntax (=>) only work for a single expression — => returns an expression, not a block.
Careful: bare Function (capitalised, no signature) effectively means "any function" and loses type checking — always write the full signature: bool Function(User).
A closure is a function plus the lexical environment it was created in. In Dart a closure captures the variable itself, not a snapshot of its value: if the variable changes later, the closure sees the new value.
This is the heart of the classic loop-capture question. In Dart, for (var i = 0; i < 3; i++) creates a fresh binding of `i` per iteration, so the collected closures print 0, 1, 2 — unlike JavaScript's var. But a variable declared outside the loop is a single binding and every closure sees the final value:
for (var i = 0; ...)→ 0, 1, 2 (a new binding per iteration)var i; for (i = 0; ...)→ 3, 3, 3 (one binding)
Closures also keep memory alive: if a callback captures an object, that object cannot be collected. A closure capturing this inside a long-lived listener is the textbook cause of a memory leak.
Interview tip. The loop-capture question is asked as a trap in Dart because most candidates give the JavaScript answer. A strong answer: "In Dart a `for` loop counter declared with `var` gets a fresh binding per iteration, so the result is 0, 1, 2. If the variable is declared outside the loop there is a single binding and the result is 3, 3, 3." Then add that for-in also gives each element a fresh binding.
A second frequent question: "Why do you prefer named over positional parameters?" — highlight the unreadability of calls like foo(true, false, true), the compile-time safety of required, and the fact that new parameters can be added later without disturbing order.
📚 Sources and documentation
- Functionsofficialdart.dev