Sparround

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.

FormSignatureCall siteWhen 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