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:
- Reducing duplication of code
- Improving clarity of the code
- Reuse of code
- Decomposing complex problems into simpler pieces
- Information hiding
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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().
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.
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.
// 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.
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.
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
List all Dart tutorials.