ZetCode

Dart function

last modified May 29, 2026

Dart function tutorial shows how to work with functions in Dart.

Dart function definition

A function is a mapping of zero or more input parameters to zero or more output parameters.

The advantages of using functions are:

Dart functions are first-class citizens. Functions can be assigned to variables, passed as arguments to functions or returned from functions. This makes the language more flexible.

The body of the function consists of statements that are executed when the function is called. We use the return keyword to return values from functions. The body is delimited with a pair of curly brackets {}. To call a function, we specify its name followed by round brackets (). A function may or may not take parameters.

Dart function simple example

The following example creates a simple function in Dart.

main.dart
void main() {
  int x = 4;
  int y = 5;

  int z = add(x, y);

  print("Output: $z");
}

int add(int a, int b) {
  return a + b;
}

In the example, we define a function which adds two values.

void main() {

The main function is the entry point of the program.

int z = add(x, y);

We call the add function; it takes two parameters. The computed value is passed to the z variable.

int add(int a, int b) {
    return a + b;
}

The definition of the add function starts with its return value type. The parameters of the function are separated with comma; each parameter name is preceded with its data type. The statements that are executed when the function is called are placed between curly brackets. The result of the addition operation is returned to the caller with the return keyword.

print("Output: $z");

The print is a built-in Dart function, which prints the given value to the console.

$ dart main.dart
Output: 9

Dart main function arguments

The main function can accept arguments from the command line.

main.dart
void main(List<String> args) {
  print(args);
  print(args.length);

  if (args.length > 1) {
    var a = args[1];
    print(a);
  }
}

The command line arguments are stored in the args list of strings.

$ dart main.dart 1 2 3 4 5
[1, 2, 3, 4, 5]
5
2

Dart arrow function

The arrow function allows us to create a simplified function consisting of a single expression. We can omit the curly brackets and the return keyword.

main.dart
int add(int x, int y) => x + y;

int sub(int x, int y) => x - y;

void main() {
  print(add(3, 5));
  print(sub(5, 4));
}

In the example, we have two functions defined with the arrow syntax.

$ dart main.dart
8
1

Dart optional positional parameter

The square brackets [] are used to specify optional positional parameters.

main.dart
void main() {
  print(pow(2, 2));
  print(pow(2, 3));
  print(pow(3));
}

int pow(int x, [int y = 2]) {
  int r = 1;
  for (int i = 0; i < y; i++) {
    r *= x;
  }
  return r;
}

We define a power function. The second parameter is optional; if it is not specified, its default value of 2 is used, computing the square of x. The loop runs exactly y times, multiplying r by x on each iteration.

$ dart main.dart
4
8
9

Dart optional named parameters

Optional named parameters are specified inside curly {} brackets.

main.dart
void main() {
  info('John Doe', occupation: 'carpenter');
  info('Jane Doe');
}

void info(String name, {String occupation = 'unknown'}) {
  print('$name works as $occupation');
}

The info function takes one required positional parameter and one optional named parameter with a default value. Named parameters must be passed by name using the colon syntax; if omitted, the default value is used instead.

info('John Doe', occupation: 'carpenter');
info('Jane Doe');

The first call supplies the optional occupation argument by name. The second call omits it entirely, so the declared default 'unknown' is used automatically.

$ dart main.dart
John Doe works as carpenter
Jane Doe works as unknown

Dart anonymous function

An anonymous function (also called a lambda or function literal) has no name. It can be stored in a variable, passed to another function, or returned. Anonymous functions support both a block body with curly brackets and the concise arrow syntax when the body is a single expression.

main.dart
void main() {
  var words = ['sky', 'cloud', 'forest', 'welcome'];

  // Block-body anonymous function
  words.forEach((String word) {
    print('$word has ${word.length} characters');
  });

  // Arrow-syntax anonymous functions – compact, single-expression form
  var nums = [1, 2, 3, 4, 5];
  var doubled = nums.map((n) => n * 2).toList();
  var evens   = nums.where((n) => n.isEven).toList();
  print(doubled);
  print(evens);
}

The block form suits multi-statement bodies. The arrow form (params) => expr suits single-expression callbacks and is the same rule that applies to named arrow functions. Both forms are anonymous: they carry no name and exist only as values.

$ dart main.dart
sky has 3 characters
cloud has 5 characters
forest has 6 characters
welcome has 7 characters
[2, 4, 6, 8, 10]
[2, 4]

Dart recursive function

Recursion, in mathematics and computer science, is a way of defining methods in which the method being defined is applied within its own definition. To put it differently, a recursive method calls itself to do its task. Recursion is a widely used approach to solve many programming tasks.

A typical example is the calculation of a factorial.

main.dart
int fact(int n) {
  if (n == 0 || n == 1) {
    return 1;
  }

  return n * fact(n - 1);
}

void main() {
  print(fact(7));
  print(fact(10));
  print(fact(15));
}

In this code example, we calculate the factorial of three numbers.

return n * fact(n - 1);

Inside the body of the fact function, we call the fact function with a modified argument. The function calls itself.

$ dart main.dart
5040
3628800
1307674368000

Dart function as parameter

A Dart function can be passed to other functions as a parameter. Such a function is called a higher-order function. The parameter type should be written as an inline function type — ReturnType Function(ParamTypes) — rather than the bare Function type, which gives up type safety.

main.dart
int inc(int x) => x + 1;
int dec(int x) => x - 1;
int square(int x) => x * x;

int apply(int x, int Function(int) op) => op(x);

void main() {
  print(apply(3, inc));    // 4
  print(apply(5, dec));    // 4
  print(apply(4, square)); // 16
}

The parameter op is declared with the inline function type int Function(int): a callable that accepts one int and returns an int. This is more type-safe than the bare Function type because the compiler verifies the callback's signature at the call site. Any named or anonymous function matching the shape can be passed.

int apply(int x, int Function(int) op) => op(x);

The syntax is ReturnType Function(ParameterTypes). Parameter names inside the type annotation are optional; only the types matter for type checking.

$ dart main.dart
4
4
16

Dart nested function

A nested function, also called an inner function, is a function defined inside another function.

main.dart
void main() {
  String buildMessage(String name, String occupation) {
    return "$name is a $occupation";
  }

  var name = "John Doe";
  var occupation = "gardener";

  var msg = buildMessage(name, occupation);
  print(msg);
}

We have a helper buildMessage function which is defined inside the main function.

$ dart main.dart 
John Doe is a gardener

Dart function returning a function (closure)

A function in Dart can return another function. This allows you to create closures that capture variables from their surrounding scope.

main.dart
Function makeAdder(int addBy) {
  return (int x) => x + addBy;
}

void main() {
  var add2 = makeAdder(2);
  var add5 = makeAdder(5);

  print(add2(3)); // 5
  print(add5(3)); // 8
}

The makeAdder function returns a new function that adds a specific value to its argument. The returned function remembers the value of addBy from its creation context.

$ dart main.dart
5
8

Dart closure variable capture

A closure captures variables from its enclosing scope by reference, not by value. Any mutation of a captured variable after the closure is created is visible inside the closure. Multiple closures can share and mutate the same variable, which is a clean way to encapsulate mutable state.

main.dart
void main() {
  // Multiple closures sharing one captured variable
  var count = 0;
  final increment = () => ++count;
  final decrement = () => --count;
  final reset     = () { count = 0; };

  increment();
  increment();
  increment();
  print(count); // 3

  decrement();
  print(count); // 2

  reset();
  print(count); // 0

  // Pitfall: capture-by-reference, not by value
  var message = 'hello';
  final greet = () => print(message); // captures the variable, not its value
  message = 'world'; // mutated after closure creation
  greet();           // world — not 'hello'
}

increment, decrement, and reset all operate on the same count variable. The greet example illustrates the key pitfall: the closure holds a live reference to message, so it sees the mutated value at call time, not the value that existed when the closure was created.

$ dart main.dart
3
2
0
world

Dart default values for named parameters

You can provide default values for named parameters in Dart functions. This makes parameters optional and assigns a default if not provided.

main.dart
void greet(String name, {String greeting = 'Hello'}) {
  print('$greeting, $name!');
}

void main() {
  greet('Alice');
  greet('Bob', greeting: 'Hi');
}

The greet function has a named parameter greeting with a default value. If not specified, 'Hello' is used.

$ dart main.dart
Hello, Alice!
Hi, Bob!

Dart function typedefs

Typedefs allow you to define a function signature and use it as a type, improving code readability and type safety.

main.dart
typedef IntOperation = int Function(int, int);

int add(int a, int b) => a + b;
int mul(int a, int b) => a * b;

void printResult(int x, int y, IntOperation op) {
  print(op(x, y));
}

void main() {
  printResult(3, 4, add);
  printResult(3, 4, mul);
}

Here, IntOperation is a typedef for a function that takes two integers and returns an integer. We use it to specify the type of the op parameter in printResult.

$ dart main.dart
7
12

Required named parameters

Named parameters are optional by default. The required keyword makes a named parameter mandatory — omitting it is a compile-time error. Required and optional named parameters can be mixed freely in the same function signature.

main.dart
String formatAddress({
  required String street,
  required String city,
  String country = 'USA',
}) {
  return '$street, $city, $country';
}

void main() {
  print(formatAddress(street: '123 Main St', city: 'Springfield'));
  print(formatAddress(street: '10 Downing St', city: 'London', country: 'UK'));
}

street and city are marked required; omitting either at the call site produces a compile-time error. The country parameter remains optional and defaults to 'USA'. This pattern is common in constructors and factory helpers where some fields are always needed but others have sensible defaults.

$ dart main.dart
123 Main St, Springfield, USA
10 Downing St, London, UK

Async functions

Marking a function with async makes it implicitly return a Future. Inside such a function, await suspends execution until the awaited future resolves, without blocking the event loop. Async functions are the backbone of I/O operations in Dart.

main.dart
Future<String> fetchGreeting(String name) async {
  // Simulate a network or database call
  await Future.delayed(const Duration(milliseconds: 50));
  return 'Hello, $name!';
}

Future<void> main() async {
  var greeting = await fetchGreeting('Alice');
  print(greeting);

  // Launch multiple async calls concurrently
  var greetings = await Future.wait([
    fetchGreeting('Bob'),
    fetchGreeting('Carol'),
  ]);
  greetings.forEach(print);
}

fetchGreeting is declared async and its return type is Future<String>. The await inside main suspends only that coroutine, not the whole program. Future.wait launches both calls concurrently and resolves when all of them complete, which is more efficient than awaiting them sequentially.

$ dart main.dart
Hello, Alice!
Hello, Bob!
Hello, Carol!

Generator functions

A sync* generator function returns a lazy Iterable. The yield keyword emits one value at a time without building the full sequence in memory, making generators ideal for infinite or expensive-to-compute sequences.

main.dart
Iterable<int> fibonacci() sync* {
  int a = 0, b = 1;
  while (true) {
    yield a;
    final next = a + b;
    a = b;
    b = next;
  }
}

void main() {
  var fibs = fibonacci().take(8).toList();
  print(fibs);

  // Generators compose naturally with Iterable methods
  var evenFibs = fibonacci().where((n) => n.isEven).take(5).toList();
  print(evenFibs);
}

The body of a sync* function is not executed until the caller starts iterating. Each yield pauses the function and hands one value to the consumer; execution resumes from that point on the next iteration step. Because fibonacci is infinite, we use take to limit consumption. The second example chains where directly on the generator, demonstrating that generators compose naturally with all Iterable combinators.

$ dart main.dart
[0, 1, 1, 2, 3, 5, 8, 13]
[0, 2, 8, 34, 144]

Function tear-offs

A tear-off is a reference to a function or method obtained without calling it — the parentheses are simply omitted. Tear-offs are first-class values that can be stored, passed, and called later. They avoid creating a redundant anonymous wrapper such as (s) => s.toUpperCase().

main.dart
void shout(String s) => print(s.toUpperCase());

void main() {
  // Top-level function tear-off
  var fn = shout;
  fn('hello'); // HELLO

  // Instance method tear-off: no () means no call
  var upper = 'hello'.toUpperCase;
  print(upper()); // HELLO

  // Static method tear-off passed to a higher-order function
  var parse = int.parse;
  var numbers = ['10', '20', '30'].map(parse).toList();
  print(numbers); // [10, 20, 30]

  // print itself is a function — pass it directly as a callback
  ['apple', 'banana', 'cherry'].forEach(print);
}

shout and 'hello'.toUpperCase are referenced without (), so no call happens at that point. int.parse is a static method tear-off passed directly to map, replacing the verbose (s) => int.parse(s). Tear-offs of the same method on the same object compare equal with ==, giving them stable identity unlike closures.

$ dart main.dart
HELLO
HELLO
[10, 20, 30]
apple
banana
cherry

Callable classes

A class that defines a call method can be invoked with function-call syntax. Instances of such a class are interchangeable with plain functions from the caller's perspective, making them ideal for stateful or configurable callable objects.

main.dart
class Multiplier {
  final int factor;
  const Multiplier(this.factor);

  int call(int value) => value * factor;
}

class RangeValidator {
  final int min;
  final int max;
  const RangeValidator({required this.min, required this.max});

  bool call(int value) => value >= min && value <= max;
}

void main() {
  final twice  = Multiplier(2);
  final thrice = Multiplier(3);

  print(twice(5));   // 10
  print(thrice(5));  // 15

  // Callable objects work wherever a matching function type is expected
  print([1, 2, 3, 4].map(twice).toList()); // [2, 4, 6, 8]

  final inRange = RangeValidator(min: 1, max: 100);
  print(inRange(50));  // true
  print(inRange(150)); // false
}

twice(5) calls Multiplier.call(5) transparently. Because Multiplier matches int Function(int), it can be passed anywhere that function type is expected — here to map. RangeValidator shows a callable with named constructor parameters, a common pattern for configurable predicate objects.

$ dart main.dart
10
15
[2, 4, 6, 8]
true
false

Generic higher-order functions

Higher-order functions become far more reusable when combined with generics. A generic type parameter lets the compiler verify that the callback and the surrounding function agree on types, catching errors at compile time rather than at runtime.

main.dart
// Apply any single-argument transformation and return the result
T transform<T>(T value, T Function(T) op) => op(value);

// Map a list from one element type to another
List<R> mapList<T, R>(List<T> list, R Function(T) f) =>
    list.map(f).toList();

// Reduce a list to a single value using a type-safe combiner
T fold<T>(List<T> list, T seed, T Function(T, T) combine) {
  var result = seed;
  for (final item in list) result = combine(result, item);
  return result;
}

void main() {
  print(transform(6, (n) => n * n));                // 36
  print(transform('dart', (s) => s.toUpperCase())); // DART

  var lengths = mapList(['cat', 'mouse', 'elephant'], (s) => s.length);
  print(lengths); // [3, 5, 8]

  var sum = fold([1, 2, 3, 4, 5], 0, (a, b) => a + b);
  print(sum); // 15
}

Dart infers type arguments from context, so the call sites stay clean. Passing a callback with the wrong signature is a compile-time error. The three utilities here mirror patterns from Dart's own standard library (Iterable.map, Iterable.fold), demonstrating how generics make function abstractions safe and reusable across any type.

$ dart main.dart
36
DART
[3, 5, 8]
15

Function equality and identity

Dart defines precise rules for function equality. Top-level functions and static methods have stable identity — two references to the same function compare equal. Closures, however, are new objects each time they are evaluated, so two independently created closures with identical code are not equal even though they behave the same way.

main.dart
void greet() => print('Hello');

void main() {
  // Top-level function references: same function == equal
  var f1 = greet;
  var f2 = greet;
  print(f1 == f2); // true

  // Closures created separately: not equal, even with identical code
  var c1 = () => print('hi');
  var c2 = () => print('hi');
  print(c1 == c2); // false

  // Same closure object assigned to two variables: equal
  var c3 = c1;
  print(c1 == c3); // true

  // Tear-offs from the same object and method: equal
  var s  = 'hello';
  var t1 = s.toUpperCase;
  var t2 = s.toUpperCase;
  print(t1 == t2); // true
}

This matters when using functions as map keys or set members, and when removing callbacks from listener lists. Store a closure in a variable and reuse that variable to guarantee stable identity; avoid creating a new closure literal each time if equality checks are required.

$ dart main.dart
true
false
true
true

Inline functions in switch expressions

Dart 3 introduced switch expressions, which can yield any value — including a function. This enables compact dispatch tables where each branch maps a selector to a different callable, as a clean alternative to chains of if/else or factory maps.

main.dart
typedef Formatter = String Function(num);

Formatter getFormatter(String style) => switch (style) {
      'currency' => (n) => '\$${n.toStringAsFixed(2)}',
      'percent'  => (n) => '${(n * 100).toStringAsFixed(1)}%',
      'round'    => (n) => n.round().toString(),
      _          => (n) => n.toString(),
    };

void main() {
  final values = [1499.5, 0.1875, 3.7];
  final styles = ['currency', 'percent', 'round'];

  for (var i = 0; i < values.length; i++) {
    final fmt = getFormatter(styles[i]);
    print(fmt(values[i]));
  }
}

Each arm of the switch expression is an anonymous arrow function. getFormatter returns the right formatter at runtime based on the style string. The wildcard _ arm provides a fallback so the switch is exhaustive — the Dart compiler enforces this, ensuring no style value is silently unhandled.

$ dart main.dart
$1499.50
18.8%
4

Source

Dart functions - language reference

In this article we have learned how to work with functions in Dart. We covered defining functions, positional and named parameters (optional, default, and required), arrow and anonymous functions, closures and variable capture, higher-order and generic functions, typedefs, function tear-offs, callable classes, function equality rules, async functions, generator functions, and inline functions in Dart 3 switch expressions.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.