Control Flow in Dart: If-Else, Switch-Case, and Decision-Making Statements Explained

Control Flow (if, else, switch) in Dart

Control Flow (if, else, switch) in Dart

Control flow is the part of programming that decides which code actually runs, and in what order — and honestly, it’s where most of the real logic of any app I build actually lives. In this article, I’m going to go through Dart’s decision-making tools in depth: if-else, the ternary operator, switch-case (including the newer pattern-matching enhancements), and the null-aware operators that have become second nature to me since Dart adopted sound null safety. I’ll back everything with real code, output, and the kind of practical context I wish I’d had when I was first learning this.

The if Statement

The simplest form of control flow is if, which runs a block of code only when a condition evaluates to true.

void main() {
  int age = 20;

  if (age >= 18) {
    print('You are an adult.');
  }
}

Output:

You are an adult.

If the condition is false, the block is simply skipped, and execution continues after it.

if-else

Most of the time, I need to handle both the “true” and “false” outcome of a condition, which is where else comes in.

void main() {
  int age = 15;

  if (age >= 18) {
    print('You are an adult.');
  } else {
    print('You are a minor.');
  }
}

Output:

You are a minor.

else if Chains

When I have more than two possible outcomes, I chain else if blocks together. Dart evaluates them top to bottom and runs the first block whose condition is true, skipping the rest.

void main() {
  int score = 72;

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

Output:

Grade: C

I’ve learned to order these conditions carefully — since Dart checks them sequentially and stops at the first match, the order genuinely matters. If I had written score >= 60 before score >= 75, the B and A branches would become unreachable for scores between 60 and 89.

The Ternary Operator

For simple conditional assignments, I almost always reach for the ternary operator instead of a full if-else block, since it keeps things compact.

void main() {
  int age = 20;
  String status = age >= 18 ? 'Adult' : 'Minor';
  print(status);
}

Output:

Adult

The syntax is condition ? valueIfTrue : valueIfFalse. I use this constantly inside Flutter’s build() methods for quick conditional widget properties, like choosing a color or text based on some state.

Text(
  isOnline ? 'Online' : 'Offline',
  style: TextStyle(color: isOnline ? Colors.green : Colors.grey),
)

Null-Aware Operators as Control Flow

Since Dart’s null safety became standard, I use a handful of null-aware operators so often that they’ve effectively become part of my everyday control flow vocabulary.

void main() {
  String? name;
  print(name ?? 'Guest');

  name = 'Hina';
  print(name ?? 'Guest');
}

Output:

Guest
Hina

?? provides a fallback value when the left-hand side is null. I also use ??= to assign a value only if a variable is currently null:

void main() {
  String? username;
  username ??= 'anonymous_user';
  print(username);
}

Output:

anonymous_user

And ?. to safely call a method or access a property only if the object isn’t null, short-circuiting to null otherwise:

void main() {
  String? city;
  print(city?.toUpperCase());
  city = 'Lahore';
  print(city?.toUpperCase());
}

Output:

null
LAHORE

These operators genuinely changed how I write conditional logic — instead of a verbose if (x != null) { ... } block every time I touch a nullable variable, I can express the same intent in a single line.

The switch Statement

switch is what I reach for when I have a single value that needs to be compared against several possible discrete cases — it’s more readable than a long else if chain when every branch is checking equality against the same variable.

void main() {
  String day = 'Wednesday';

  switch (day) {
    case 'Monday':
      print('Start of the work week.');
      break;
    case 'Wednesday':
      print('Midweek check-in.');
      break;
    case 'Friday':
      print('Almost the weekend!');
      break;
    default:
      print('Just another day.');
  }
}

Output:

Midweek check-in.

A few things I keep in mind with switch:

void main() {
  String day = 'Saturday';

  switch (day) {
    case 'Saturday':
    case 'Sunday':
      print('It\'s the weekend!');
      break;
    default:
      print('It\'s a weekday.');
  }
}

Output:

It's the weekend!

Modern Dart: switch Expressions and Pattern Matching

More recent versions of Dart introduced switch as an expression, not just a statement, along with pattern matching, and I’ve come to really enjoy using it for concise, exhaustive logic.

String describeNumber(int number) {
  return switch (number) {
    0 => 'Zero',
    1 || 2 || 3 => 'Small number',
    int n when n > 100 => 'Large number',
    _ => 'Some other number',
  };
}

void main() {
  print(describeNumber(0));
  print(describeNumber(2));
  print(describeNumber(150));
  print(describeNumber(50));
}

Output:

Zero
Small number
Large number
Some other number

This switch expression directly returns a value, using => for each case instead of a colon and a break. The || pattern lets me match multiple values in one case, when adds a guard condition, and _ acts as the catch-all, similar to default. I’ve found this style especially useful when working with sealed classes, since Dart can verify at compile time that every possible subtype has been handled, warning me if I’ve missed a case.

sealed class Shape {}

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

class Square extends Shape {
  final double side;
  Square(this.side);
}

double calculateArea(Shape shape) {
  return switch (shape) {
    Circle(radius: var r) => 3.14159 * r * r,
    Square(side: var s) => s * s,
  };
}

void main() {
  print(calculateArea(Circle(4)));
  print(calculateArea(Square(5)));
}

Output:

50.26544
25.0

Because Shape is sealed, Dart knows exactly which subclasses exist, and it will actually give me a compile-time warning if I forget to handle one of them in the switch expression — a genuinely nice safety net when working with modeled data like API response states (Loading, Success, Error).

Real-World Use Case: Handling App State

This is a pattern I use extensively in Flutter apps when managing UI states based on data-loading results:

enum LoadState { loading, success, error }

Widget buildContent(LoadState state, String? data, String? errorMessage) {
  switch (state) {
    case LoadState.loading:
      return const CircularProgressIndicator();
    case LoadState.success:
      return Text(data ?? 'No data');
    case LoadState.error:
      return Text('Error: ${errorMessage ?? "Unknown error"}');
  }
}

Because LoadState is an enum, and I’m switching over all of its values without a default case, Dart’s analyzer will actually warn me if I add a new value to the enum later and forget to handle it here — genuinely useful when a codebase grows and enums get extended.

Common Mistakes

Best Practices

Debugging Control Flow

When a conditional isn’t behaving the way I expect, my first move is almost always to add temporary print() statements right before the condition, showing the actual values being compared. It sounds almost too simple, but the vast majority of control-flow bugs I’ve hit come down to a value not being what I assumed it was — a string with unexpected whitespace, a number that’s actually a double being compared against an int pattern, or a boolean that was never actually flipped where I thought it was. I also use the debugger’s breakpoints in cases where the flow is genuinely complex, stepping through each branch to see exactly which path gets taken.

Frequently Asked Questions

Can I use switch on strings and doubles in Dart, or only on integers? Dart’s switch supports comparing against integers, strings, and enum values, along with objects that have a properly implemented == operator, and now more general patterns with switch expressions.

Does Dart support fall-through in switch statements like C does? Not implicitly — each non-empty case must end in break, return, continue, or throw. Explicit fall-through is possible only by stacking empty case labels together or using continue with a label pointing to another case.

What’s the real difference between a switch statement and a switch expression? A switch statement executes a block of code per case, using : and requiring a terminating statement like break. A switch expression directly evaluates to a value using =>, is often more concise, and is checked for exhaustiveness by the compiler when used with enums or sealed types.

When should I use ?? versus a full if (x != null) check? Use ?? when you just need a fallback value for a single expression. Use a full if check when the null case requires more complex logic than simply substituting a default value.

Summary

Control flow is where the actual decision-making of my apps happens, and Dart gives me a genuinely rich toolkit for it — from simple if-else chains, to the compact ternary operator, to switch statements and modern switch expressions with real pattern matching and exhaustiveness checking. Combined with null-aware operators that make handling optional values far less verbose, I’ve found that Dart’s control flow tools consistently let me write logic that’s both concise and safe, with the compiler catching a surprising number of mistakes before they ever become runtime bugs.

References

Exit mobile version