Skip to content

Dart Functions — Parameters, Return Types, and Higher-Order Functions

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Dart Functions. We cover key concepts, practical examples, and best practices to help you master this topic.

Dart functions are first-class objects that can be assigned to variables, passed as arguments, and returned from other functions, with support for optional parameters, type parameters, and concise arrow syntax.

What You Will Learn

  • Declaring functions with parameters and return types
  • Positional vs named vs optional parameters
  • Arrow syntax for concise function bodies
  • Anonymous functions and closures
  • Higher-order functions that accept or return functions
  • Lexical scoping and variable capture
  • Using typedef for function type aliases

Why It Matters

Functions are the building blocks of readable, maintainable code. Dart's function system is designed to be expressive while remaining type-safe. Named parameters eliminate the need for overloaded functions. Arrow syntax reduces boilerplate for simple transformations. Higher-order functions enable Functional Programming patterns like map, filter, and reduce. Mastering functions is essential before moving to Flutter, where widgets are composed using callback functions extensively.

Real-World Use

In the DodaTech Flutter app, every screen uses callbacks for user interactions. Button onPressed handlers are anonymous functions, API callbacks use higher-order functions for success and error handling, and the state management layer uses function composition for middleware.

Learning Path

flowchart LR
  A[Control Flow] --> B[Dart Functions\nYou are here]
  B --> C[Dart Collections]
  style B fill:#f90,color:#fff

Basic Function Syntax

A Dart function consists of a return type, a name, a parameter list, and a body:

// Function with explicit return type and parameter types
int add(int a, int b) {
  return a + b;
}

// Arrow syntax for single-expression functions
int multiply(int a, int b) => a * b;

// Void return type
void greet(String name) {
  print('Hello, $name!');
}

void main() {
  print('Add: ${add(5, 3)}');
  print('Multiply: ${multiply(4, 6)}');
  greet('Alice');
}

Output:

Add: 8
Multiply: 24
Hello, Alice!

Arrow syntax (=>) is preferred for functions that consist of a single expression. The return keyword is implicit with arrow syntax. Functions with multiple statements use block body with {} and explicit return.

Positional Parameters

Parameters are positional by default. Required parameters must be provided in order:

void main() {
  printFullName('Alice', 'Johnson');
}

void printFullName(String first, String last) {
  print('$first $last');
}

Output: Alice Johnson

All parameters are required unless marked optional. The compiler enforces that all required arguments are provided at the call site.

Optional Positional Parameters

Use square brackets to mark parameters as optional positional:

void main() {
  introduce('Bob');
  introduce('Carol', 25);
  introduce('Dave', 30, 'New York');
}

void introduce(String name, [int? age, String? city]) {
  var result = 'Hi, I am $name';
  if (age != null) result += ', $age years old';
  if (city != null) result += ', from $city';
  print('$result.');
}

Output:

Hi, I am Bob.
Hi, I am Bob, 25 years old.
Hi, I am Bob, 30 years old, from New York.

Optional positional parameters must be nullable unless they have a default value. They are declared inside [] and appear after all required parameters.

Named Parameters

Named parameters use curly braces and can be provided in any order:

void main() {
  // Named parameters in any order
  createUser(
    name: 'Alice',
    age: 30,
    email: 'alice@example.com',
  );

  // Omitting optional named parameters
  createUser(
    name: 'Bob',
    email: 'bob@example.com', // age is omitted
  );
}

void createUser({
  required String name,
  int? age,
  required String email,
}) {
  print('Created user: $name, $email');
  if (age != null) print('Age: $age');
}

Output:

Created user: Alice, alice@example.com
Age: 30
Created user: Bob, bob@example.com

Use required for named parameters that must always be provided. Named parameters without required and without a default value are optional and nullable. Named parameters improve readability at call sites, especially for functions with many parameters.

Default Parameter Values

Both positional and named parameters can have default values:

void main() {
  connectToServer();
  connectToServer(port: 8080);
  connectToServer(host: '192.168.1.1', port: 9090);
}

void connectToServer({
  String host = 'localhost',
  int port = 3000,
  bool useTls = false,
}) {
  print('Connecting to $host:$port (TLS: $useTls)');
}

Output:

Connecting to localhost:3000 (TLS: false)
Connecting to localhost:8080 (TLS: false)
Connecting to 192.168.1.1:9090 (TLS: false)

Default values must be compile-time constants. They are evaluated once at the call site, not once per function declaration.

Anonymous Functions

Anonymous functions (lambdas) are functions without a name. They are commonly used as callbacks:

void main() {
  // Anonymous function assigned to a variable
  var square = (int x) => x * x;
  print('Square of 7: ${square(7)}');

  // Anonymous function as a callback
  var numbers = [1, 2, 3, 4, 5];
  var doubled = numbers.map((n) => n * 2).toList();
  print('Doubled: $doubled');

  // Multi-line anonymous function
  numbers.forEach((n) {
    var squared = n * n;
    print('$n squared is $squared');
  });
}

Output:

Square of 7: 49
Doubled: [2, 4, 6, 8, 10]
1 squared is 1
2 squared is 4
3 squared is 9
...

Anonymous functions have the same type system as named functions. The parameter types can be inferred from context, making the syntax concise.

Higher-Order Functions

A higher-order function is a function that takes another function as a parameter, returns a function, or both:

void main() {
  var numbers = [1, 2, 3, 4, 5, 6];

  // filter: keeps elements matching a predicate
  var evens = numbers.where((n) => n.isEven).toList();
  print('Evens: $evens');

  // map: transforms each element
  var strings = numbers.map((n) => 'Number: $n').toList();
  print(strings);

  // reduce: combines elements into a single value
  var sum = numbers.reduce((a, b) => a + b);
  print('Sum: $sum');

  // Custom higher-order function
  var result = applyOperation(10, 5, (a, b) => a * b);
  print('Operation result: $result');
}

int applyOperation(int a, int b, int Function(int, int) operation) {
  return operation(a, b);
}

Output:

Evens: [2, 4, 6]
[Number: 1, Number: 2, Number: 3, Number: 4, Number: 5, Number: 6]
Sum: 21
Operation result: 50

The where, map, and reduce methods on collections are built-in higher-order functions. Custom higher-order functions accept callbacks using Function types or typedef.

Closures

A closure is a function that captures variables from its surrounding lexical scope:

void main() {
  // makeCounter returns a closure
  var counter = makeCounter();
  print(counter()); // 1
  print(counter()); // 2
  print(counter()); // 3

  // Each closure has its own captured state
  var anotherCounter = makeCounter();
  print(anotherCounter()); // 1 (independent)
}

Function makeCounter() {
  int count = 0;
  return () {
    count++;
    return count;
  };
}

Output:

1
2
3
1

The closure captures the count variable. Even though makeCounter has returned, the closure maintains access to count. Each call to makeCounter() creates a new count variable that is independent of others.

Typedef

typedef creates a type alias for function types, making signatures easier to read:

// Define a function type
typedef IntOperation = int Function(int a, int b);

int add(int a, int b) => a + b;
int subtract(int a, int b) => a - b;

int compute(int a, int b, IntOperation op) {
  print('Computing with $a and $b');
  return op(a, b);
}

void main() {
  print(compute(10, 5, add)); // 15
  print(compute(10, 5, subtract)); // 5
  print(compute(10, 5, (a, b) => a * b)); // 50
}

Output:

Computing with 10 and 5
15
Computing with 10 and 5
5
Computing with 10 and 5
50

Without typedef, the compute function would need int Function(int, int) op as the parameter type. typedef makes the intent clearer and reduces duplication.

Generator Functions

Generator functions produce a sequence of values lazily using sync* and yield:

void main() {
  var numbers = countUpTo(5);
  for (var n in numbers) {
    print('Generated: $n');
  }
}

Iterable<int> countUpTo(int max) sync* {
  int i = 1;
  while (i <= max) {
    yield i;
    i++;
  }
}

Output:

Generated: 1
Generated: 2
Generated: 3
Generated: 4
Generated: 5

The sync* keyword marks a function as a synchronous generator. yield produces each value. The iteration is lazy: values are computed only when requested.

Common Mistakes

  1. Omitting required for named parameters that should be mandatory: Named parameters are optional by default. Add required to ensure callers provide the parameter.

  2. Using Function without type parameters: Function accepts any function signature. Use typed versions like void Function(String) or typedef for type safety.

  3. Confusing => with => as a comparison: The arrow => is not a comparison operator. It defines the function body. Comparison is ==, >=, <=.

  4. Modifying captured variables in closures unintentionally: Closures capture variables by reference. Changing a captured variable in one closure affects all closures that captured the same variable.

  5. Not using required with named parameters for constructors: Constructor parameters should use required or default values. Omitting both makes the parameter nullable and creates the possibility of incomplete object initialization.

Practice Questions

  1. What is the difference between positional and named parameters?
  2. How does arrow syntax differ from block body syntax?
  3. What is a closure and what does it capture?
  4. How does sync* differ from a regular function that returns a List?
  5. Challenge: Implement a createValidator function that takes a validation rule (a function from String to String?) and returns a function that validates a list of strings. The returned function should collect all validation errors and return them as a list.

Mini Project

Build a string processing library:

  • A transform higher-order function that accepts a string and a transformation function
  • Pre-defined transformations: uppercase, lowercase, reverse, capitalize
  • A pipeline function that chains multiple transformations
  • A filter function that keeps strings matching a predicate
  • Use typedef for common function signatures
  • Write unit tests for each function

FAQ

Can a Dart function return multiple values?

Yes. Use a Record (Dart 3+): (String, int) getPerson() => ('Alice', 30);. Records can have named fields too.

What is the difference between `Function` and `void Function()`?

Function accepts any callable. void Function() accepts only functions with no parameters and no return value. Always prefer the more specific type.

Can I overload functions in Dart?

No. Dart does not support function overloading. Use named parameters or different function names instead.

How do I define a function that takes a variable number of arguments?

Use optional positional parameters with [] or accept a List as a parameter. Optional positional parameters can have defaults.

What is the `@pragma` annotation above some functions?

@pragma('vm:entry-point') and similar annotations provide hints to the Dart VM and compiler. They are not related to function declaration syntax.

What is Next

Now that you understand functions, learn how to work with data collections. Proceed to Collections in Dart for lists, sets, maps, and collection operations. Then explore Classes in Dart for object-oriented programming.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro