Skip to content

Dart Control Flow — Conditions, Loops, and Switch

DodaTech Updated 2026-06-28 8 min read

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

Dart control flow statements including if-else, for and while loops, and switch expressions allow you to execute different code paths based on conditions and iterate over collections.

What You Will Learn

  • Writing if-else and ternary conditional expressions
  • Using for, for-in, and while loops
  • The enhanced switch statement in Dart 3
  • Pattern matching with switch and if-case
  • Breaking and continuing loop execution
  • Exception Handling with try-catch-finally

Why It Matters

Control flow is how programs make decisions and repeat operations. Dart's control flow features are designed for clarity and safety. The enhanced switch statement in Dart 3 supports pattern matching, exhaustiveness checking, and expression syntax, making it more powerful than switch in most other languages. Understanding these constructs lets you write code that is both correct and readable.

Real-World Use

The DodaTech Flutter app uses switch expressions extensively for handling UI state. A ViewState sealed class with Loading, Loaded, and Error variants is matched with switch, and the compiler ensures every case is handled. This eliminates the possibility of unhandled states in production.

Learning Path

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

If-Else Statements

The if-else statement evaluates a boolean condition and executes the corresponding branch:

void main() {
  int score = 85;

  if (score >= 90) {
    print('Grade: A');
  } else if (score >= 80) {
    print('Grade: B');
  } else if (score >= 70) {
    print('Grade: C');
  } else {
    print('Grade: F');
  }

  // Ternary conditional
  String status = score >= 60 ? 'Pass' : 'Fail';
  print('Status: $status');
}

Output:

Grade: B
Status: Pass

The ternary operator condition ? expr1 : expr2 is useful for simple binary choices. For complex conditions, use if-else with braces for clarity.

For Loops

Dart provides several loop forms for iterating over ranges and collections:

void main() {
  // Traditional for loop
  for (int i = 0; i < 5; i++) {
    print('Count: $i');
  }

  // For-in loop with list
  var fruits = ['apple', 'banana', 'cherry'];
  for (var fruit in fruits) {
    print('Fruit: $fruit');
  }

  // For-in loop with range
  for (var i in [1, 2, 3, 4, 5]) {
    print('Square of $i: ${i * i}');
  }

  // forEach with lambda
  fruits.forEach((fruit) => print('Got: $fruit'));
}

Output:

Count: 0
Count: 1
Count: 2
Count: 3
Count: 4
Fruit: apple
Fruit: banana
Fruit: cherry
Square of 1: 1
Square of 2: 4
...

The for-in loop is the most common iteration pattern in Dart. Use it with any iterable collection. The forEach method is available on collections but is less idiomatic than for-in in most cases.

While and Do-While Loops

While loops repeat as long as a condition is true. Do-while loops execute the body at least once:

void main() {
  // While loop
  int count = 0;
  while (count < 3) {
    print('While count: $count');
    count++;
  }

  // Do-while loop
  int value = 5;
  do {
    print('Do-while: $value');
    value--;
  } while (value > 0);

  // Infinite loop with break
  int attempts = 0;
  while (true) {
    attempts++;
    if (attempts >= 3) {
      print('Breaking after $attempts attempts');
      break;
    }
  }
}

Output:

While count: 0
While count: 1
While count: 2
Do-while: 5
Do-while: 4
Do-while: 3
Do-while: 2
Do-while: 1
Breaking after 3 attempts

Use break to exit a loop early. Use continue to skip the rest of the current iteration and proceed to the next.

Enhanced Switch (Dart 3)

Dart 3 introduced a powerful switch statement that supports pattern matching, exhaustiveness, and expression syntax:

void main() {
  // Traditional switch
  String day = 'Monday';
  switch (day) {
    case 'Saturday':
    case 'Sunday':
      print('Weekend');
      break;
    default:
      print('Weekday');
  }

  // Enhanced switch expression (Dart 3)
  String description = switch (day) {
    'Saturday' || 'Sunday' => 'Weekend',
    'Monday' || 'Friday' => 'Bordering weekend',
    _ => 'Midweek',
  };
  print('Day type: $description');
}

Output:

Weekday
Day type: Bordering weekend

Switch expressions with => are more concise than statement switch. The _ wildcard matches any value (like default). The compiler checks exhaustiveness: if you match on a sealed type, every variant must have a case.

Pattern Matching with Switch

Switch patterns can destructure objects and match on types:

sealed class Shape {}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);
}

class Rectangle extends Shape {
  final double width;
  final double height;
  Rectangle(this.width, this.height);
}

double calculateArea(Shape shape) {
  return switch (shape) {
    Circle(radius: var r) => 3.14159 * r * r,
    Rectangle(width: var w, height: var h) => w * h,
  };
}

void main() {
  Shape circle = Circle(5);
  Shape rect = Rectangle(4, 6);
  print('Circle area: ${calculateArea(circle)}');
  print('Rectangle area: ${calculateArea(rect)}');
}

Output:

Circle area: 78.53975
Rectangle area: 24

The sealed class Shape defines a closed set of subtypes. The switch expression destructures each variant and extracts the relevant fields. The compiler verifies that all subtypes are covered.

If-Case Pattern Matching

Dart 3 also supports if-case for matching a single pattern:

void main() {
  Object value = 'Hello, Dart!';

  if (value is String) {
    print('String with length ${value.length}: $value');
  }

  // if-case with destructuring
  var record = (name: 'Alice', age: 30);
  if (record case (name: var name, age: var age)) {
    print('Name: $name, Age: $age');
  }

  // Guard clause
  List<int> numbers = [1, 2, 3];
  if (numbers case [var first, ...] when first > 0) {
    print('First element is positive: $first');
  }
}

Output:

String with length 11: Hello, Dart!
Name: Alice, Age: 30
First element is positive: 1

The when clause adds an additional condition to a pattern. It is useful for combining pattern matching with boolean checks.

Exception Handling

Dart uses try-catch-finally for exception handling. Exceptions are unchecked (no checked exceptions):

void main() {
  try {
    var result = divide(10, 0);
    print('Result: $result');
  } on IntegerDivisionByZeroException {
    print('Cannot divide by zero');
  } catch (e) {
    print('Unexpected error: $e');
  } finally {
    print('Cleanup: always executes');
  }
}

int divide(int a, int b) {
  if (b == 0) {
    throw IntegerDivisionByZeroException();
  }
  return a ~/ b;
}

Output:

Cannot divide by zero
Cleanup: always executes

Use on to catch specific exception types. Use catch to handle any exception. Use finally for cleanup that must run regardless of success or failure.

Assertions

Assertions in Dart check conditions during development and are removed in production:

void main() {
  int age = -5;
  // assert(age >= 0, 'Age must be non-negative');
  // Uncommenting the above will throw AssertionError in debug mode

  // In Dart, assertions are used with the assert function
  String name = 'Alice';
  assert(name.isNotEmpty, 'Name must not be empty');
  print('Name is valid: $name');
}

Assertions only execute in debug mode. They are ignored in production AOT-compiled code.

Common Mistakes

  1. Forgetting break in switch statements: Without break, execution falls through to the next case. Dart 3's switch expressions with => avoid this pitfall entirely.

  2. Using = instead of == in conditions: The assignment operator = does not return a boolean. Dart flags this as a warning in conditions, but it is still a common typo.

  3. Modifying a collection while iterating: Adding or removing elements from a list during a for-in loop throws ConcurrentModificationError. Use a separate list to collect items to add or remove.

  4. Not handling all cases in exhaustiveness checks: When using switch on a sealed class, every variant must have a case. Add a _ => default if the sealed class may be extended later.

  5. Catching exceptions too broadly: catch (e) without on catches all exceptions including out-of-memory and stack overflow errors. Catch specific exception types whenever possible.

Practice Questions

  1. What is the difference between switch statements and switch expressions in Dart 3?
  2. How does the when clause work in pattern matching?
  3. Why should you avoid catching Error subtypes (like OutOfMemoryError)?
  4. How does the sealed class modifier improve switch exhaustiveness checking?
  5. Challenge: Write a Dart program that reads a list of strings representing shapes ("circle:5", "rect:4x6"), parses them using pattern matching, and calculates the total area. Use switch expressions and sealed classes.

Mini Project

Build a command-line grading system:

  • Read student scores (list of integers between 0 and 100)
  • Use if-else to assign letter grades (A-F)
  • Calculate average, highest, and lowest scores
  • Use switch to print a performance description for each grade
  • Handle invalid scores (negative or >100) with exceptions
  • Format output as a table

FAQ

Does Dart support fallthrough in switch?

Yes, but only in statement switch with empty cases. Use continue with a label to fall through to a specific case. Switch expressions do not support fallthrough.

What is the difference between `for` and `for-in`?

for gives you an index variable and is useful when you need the index. for-in iterates over elements directly and is simpler for most collection iteration.

Can I use pattern matching with lists?

Yes. Dart 3 supports list patterns: if (list case [var first, var second]) matches a list with exactly two elements. Use ... for rest elements.

How do I handle multiple exception types?

Use multiple on clauses: on FormatException { ... } on IOException { ... } catch (e) { ... }. The first matching handler executes.

Is there a `when` statement like Kotlin?

Dart's enhanced switch with pattern matching covers most use cases for a when construct. For simple conditions, if-else chains remain the standard approach.

What is Next

Now that you can control program flow, learn how to organize reusable code. Proceed to Functions in Dart for parameters, return types, and anonymous functions. Then explore Collections in Dart for lists, sets, and maps.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro