Dart 9 🎯 Control Flow: if/else, Ternary Expressions, and Guard Clauses
Control flow determines which code runs under which conditions. Dart provides the familiar if/else construct, the ternary conditional expression (condition ? a : b), and the switch statement with pattern matching in Dart 3. Beyond these, the language encourages a style of early exits — guard clauses — that flattens deeply nested conditionals into a linear sequence of checks. This style is not a language feature but a discipline, and Dart’s null safety, type promotion, and pattern matching make it especially effective.
Understanding control flow in Dart means understanding how expressions and statements differ, when a conditional can be an expression (and therefore usable in a variable assignment or a widget tree), and how to structure branches so that the happy path stays at the shallowest level of indentation. The choice between if/else and a ternary is not merely stylistic: expressions compose with other expressions, statements do not, and in a language where widget trees are expressions, this distinction has practical consequences.
This chapter covers if/else syntax, else if chains, the ternary conditional expression, conditional expressions with the null-coalescing operator, guard clauses, switch statements and expressions, and the patterns that keep conditional logic readable.
Key point: Dart distinguishes statements from expressions. if/else is a statement; the ternary ? : is an expression and can appear anywhere a value is expected. Guard clauses invert conditions and return early to avoid nesting. Dart 3’s switch is both a statement and an expression, with pattern matching and exhaustiveness checking.
Why control flow constructs exist
The branching problem. A program must decide between alternatives. Without conditionals, every execution path would be linear, and the program could not respond to input, state, or errors. Conditionals are the mechanism for branching, and they are the foundation of decision-making in any language.
The expression-versus-statement problem. Some conditionals produce a value; others perform an action. In Dart, the ternary operator produces a value and can be used inside expressions. The if statement does not produce a value and cannot. This distinction determines where each can appear: a ternary inside a widget tree, an if in a function body.
The nesting problem. Deeply nested if statements are hard to read. Each level of nesting adds a condition to keep in mind, and the happy path drifts to the right of the page. Guard clauses invert the conditions and exit early, leaving the main logic at the top level.
The exhaustiveness problem. When branching on an enum or a sealed class, the compiler can verify that all cases are handled. Dart 3’s switch expressions enforce exhaustiveness, which means a missing case is a compile error rather than a runtime fallthrough.
The readability problem. Conditional logic is where bugs hide. A condition that is subtly wrong, an else that is attached to the wrong if, a case that is never reached — these are errors that careful structure prevents. Dart’s syntax makes some structures easier to read and others harder, and choosing the right one for the situation is part of writing clear code.
a. The if/else statement
The if statement evaluates a boolean condition and executes a block if the condition is true. The else block executes otherwise.
if (temperature > 30) {
print('Hot');
} else {
print('Not hot');
}
The condition must be a bool. Dart does not allow truthy or falsy values, unlike JavaScript.
if (name) { } // Error: name is String, not bool
if (name != null) { } // Correct
if (name.isNotEmpty) { } // Correct
This strictness eliminates a class of bugs where a value that is not false but also not true is treated as truthy.
An else if chain handles multiple conditions:
if (score >= 90) {
grade = 'A';
} else if (score >= 80) {
grade = 'B';
} else if (score >= 70) {
grade = 'C';
} else {
grade = 'F';
}
Each condition is evaluated in order until one is true. The else at the end is optional; if omitted and no condition matches, nothing happens.
A single statement after if or else can omit braces:
if (isValid) print('valid');
else print('invalid');
The style guide recommends braces even for single statements, because adding a second statement later without adding braces is a common source of bugs.
b. Ternary conditional expression
The ternary operator is an expression: it produces a value. Its syntax is condition ? valueIfTrue : valueIfFalse.
var status = isActive ? 'active' : 'inactive';
Because it is an expression, it can appear anywhere a value is expected:
Text(isLoggedIn ? 'Welcome back' : 'Please log in')
var fee = isMember ? 0.0 : 9.99;
return items.isEmpty ? const EmptyState() : ListView(children: items);
This is why the ternary is used heavily in Flutter: widget trees are expressions, and a conditional that selects a widget must be an expression.
Ternaries can be nested, but nesting more than two levels hurts readability:
var grade = score >= 90 ? 'A'
: score >= 80 ? 'B'
: score >= 70 ? 'C'
: 'F';
When nesting exceeds two levels, an if/else chain or a switch is clearer. The ternary is best for simple either-or choices.
The ternary composes with the null-coalescing operator ??:
var display = name ?? (isGuest ? 'Guest' : 'User');
The parentheses make the grouping explicit; without them, the precedence of ?? and ?: can be confusing.
c. Guard clauses
A guard clause is an early return that handles a special case, leaving the main logic unindented. It inverts the condition: instead of if (valid) { ...many lines... }, it writes if (!valid) return;.
Nested version:
String describe(User? user) {
if (user != null) {
if (user.isActive) {
if (user.name.isNotEmpty) {
return 'Active user: ${user.name}';
} else {
return 'Active user with no name';
}
} else {
return 'Inactive user';
}
} else {
return 'No user';
}
}
Guard clause version:
String describe(User? user) {
if (user == null) return 'No user';
if (!user.isActive) return 'Inactive user';
if (user.name.isEmpty) return 'Active user with no name';
return 'Active user: ${user.name}';
}
The guard version has no nesting. Each condition is checked once, the special cases are handled at the top, and the main logic is at the bottom with no indentation.
Dart’s type promotion makes guard clauses especially effective with null safety. After if (user == null) return;, the variable is promoted to non-nullable for the rest of the function:
String greet(String? name) {
if (name == null) return 'Hello, Guest!';
return 'Hello, $name!'; // name promoted to String
}
The guard clause both handles the null case and narrows the type, so the rest of the function does not need ! or ?..
Guard clauses work for any early-exit condition: null checks, empty collections, invalid input, unauthorized access, and error states.
Future<Response> fetchData(String url) async {
if (url.isEmpty) throw ArgumentError('url required');
if (!isAuthorized()) throw StateError('unauthorized');
final response = await http.get(Uri.parse(url));
if (response.statusCode != 200) {
throw HttpException('status ${response.statusCode}');
}
return response;
}
d. Switch statements and expressions
Dart 3 introduced pattern matching in switch, making it both a statement and an expression. The switch statement executes statements based on the value; the switch expression produces a value.
Switch statement:
switch (command) {
case 'start':
start();
case 'stop':
stop();
case 'pause':
pause();
default:
print('Unknown command');
}
Dart 3 removed the need for break in most cases. Each case does not fall through by default, which eliminates a common source of bugs in C-style switches.
Switch expression:
var label = switch (status) {
Status.pending => 'Pending',
Status.active => 'Active',
Status.closed => 'Closed',
};
The switch expression must be exhaustive when the scrutinee is an enum or sealed type. The compiler verifies that every case is handled. If a case is missing, it is a compile error rather than a runtime fallthrough.
Pattern matching extends switch beyond simple equality:
var description = switch (value) {
int n when n < 0 => 'negative integer',
int n => 'integer: $n',
String s => 'string: $s',
List() => 'a list',
_ => 'something else',
};
The when clause adds a condition to a pattern. The wildcard _ matches anything and is required when the patterns are not exhaustive.
Switch expressions are expressions, so they can appear in assignments, widget trees, and return statements:
Widget buildIcon(Status status) {
return switch (status) {
Status.pending => const Icon(Icons.hourglass),
Status.active => const Icon(Icons.check),
Status.closed => const Icon(Icons.close),
};
}
e. Conditional expressions and comprehensions
Collections support if and for inside list, set, and map literals, which is a form of conditional that produces a collection rather than a statement.
var items = [
'always',
if (includeExtra) 'extra',
for (var i in range) 'item $i',
];
The if inside a collection literal is not the if statement; it is a collection-if that includes or omits an element. The for is a collection-for that iterates.
This is a form of expression-based conditional that fits naturally with Dart’s preference for expressions in value positions. A list of widgets in Flutter often uses collection-if to conditionally include children:
Column(
children: [
const Header(),
if (isLoggedIn) const UserMenu(),
const Footer(),
],
)
The if inside the list includes the widget only when the condition is true. There is no else for collection-if; a ternary can be used when both branches are needed:
Column(
children: [
isLoggedIn ? const UserMenu() : const LoginButton(),
],
)
Complete Example Session
// ============================================
// PART 1: BASIC IF/ELSE
// ============================================
void main() {
var temperature = 35;
if (temperature > 30) {
print('Hot');
} else {
print('Not hot');
}
}
// ============================================
// PART 2: ELSE IF CHAIN
// ============================================
String grade(int score) {
if (score >= 90) return 'A';
else if (score >= 80) return 'B';
else if (score >= 70) return 'C';
else return 'F';
}
void main() {
print(grade(85)); // B
}
// ============================================
// PART 3: TERNARY EXPRESSION
// ============================================
void main() {
var isLoggedIn = true;
var message = isLoggedIn ? 'Welcome back' : 'Please log in';
print(message); // Welcome back
}
// ============================================
// PART 4: TERNARY IN EXPRESSION CONTEXT
// ============================================
double calculateFee(bool isMember) {
return isMember ? 0.0 : 9.99;
}
void main() {
print(calculateFee(true)); // 0.0
print(calculateFee(false)); // 9.99
}
// ============================================
// PART 5: NESTED TERNARY
// ============================================
String letterGrade(int score) {
return score >= 90 ? 'A'
: score >= 80 ? 'B'
: score >= 70 ? 'C'
: 'F';
}
void main() {
print(letterGrade(95)); // A
}
// ============================================
// PART 6: NESTED IF (BEFORE GUARD CLAUSES)
// ============================================
String describeOld(User? user) {
if (user != null) {
if (user.isActive) {
return 'Active: ${user.name}';
} else {
return 'Inactive';
}
} else {
return 'No user';
}
}
class User {
final String name;
final bool isActive;
User(this.name, this.isActive);
}
// ============================================
// PART 7: GUARD CLAUSES (AFTER)
// ============================================
String describe(User? user) {
if (user == null) return 'No user';
if (!user.isActive) return 'Inactive';
return 'Active: ${user.name}';
}
void main() {
print(describe(null)); // No user
print(describe(User('Alice', false))); // Inactive
print(describe(User('Bob', true))); // Active: Bob
}
// ============================================
// PART 8: GUARD CLAUSE WITH TYPE PROMOTION
// ============================================
String greet(String? name) {
if (name == null) return 'Hello, Guest!';
return 'Hello, $name!'; // name promoted to String
}
void main() {
print(greet(null)); // Hello, Guest!
print(greet('Alice')); // Hello, Alice!
}
// ============================================
// PART 9: SWITCH STATEMENT
// ============================================
enum Status { pending, active, closed }
void handle(Status status) {
switch (status) {
case Status.pending:
print('Waiting...');
case Status.active:
print('Running');
case Status.closed:
print('Done');
}
}
void main() {
handle(Status.active); // Running
}
// ============================================
// PART 10: SWITCH EXPRESSION WITH PATTERNS
// ============================================
String describe2(Object value) {
return switch (value) {
int n when n < 0 => 'negative integer',
int n => 'integer: $n',
String s => 'string: $s',
List() => 'a list',
_ => 'something else',
};
}
void main() {
print(describe2(-5)); // negative integer
print(describe2(42)); // integer: 42
print(describe2('hello')); // string: hello
print(describe2([1, 2])); // a list
}
These ten parts cover basic conditionals, else-if chains, ternaries, ternaries in expression context, nested ternaries, nested ifs, guard clauses, type promotion with guards, switch statements, and switch expressions with pattern matching.
Quick Reference
Conditional Forms
| Form | Type | Produces Value |
|---|---|---|
if/else | Statement | No |
condition ? a : b | Expression | Yes |
switch statement | Statement | No |
switch expression | Expression | Yes |
if in collection | Collection element | Yes (the element) |
Comparison
| Construct | Use When |
|---|---|
if/else | Multiple statements per branch |
| Ternary | Simple either-or value |
| Nested ternary | Two levels maximum |
| Guard clause | Early exit for special cases |
switch statement | Multiple cases with statements |
switch expression | Value selection with exhaustiveness |
Guard Clause Pattern
| Step | Code |
|---|---|
| Check special case | if (x == null) return ...; |
| Check invalid state | if (!valid) return ...; |
| Main logic | Unindented, at the end |
Switch Expression
| Feature | Example |
|---|---|
| Basic case | Status.active => 'Active' |
| Guard | int n when n < 0 => 'negative' |
| Wildcard | _ => 'other' |
| Exhaustive | Compiler enforces for enums and sealed |
Best Practices
✅ Do This:
// Guard clause for early exit
if (user == null) return 'No user';
if (!user.isActive) return 'Inactive';
return 'Active: ${user.name}';
// Ternary for simple choice
var label = isActive ? 'Active' : 'Inactive';
// Switch expression for enum
var icon = switch (status) {
Status.pending => Icons.hourglass,
Status.active => Icons.check,
Status.closed => Icons.close,
};
// Collection-if for conditional widgets
children: [
const Header(),
if (isLoggedIn) const UserMenu(),
]
❌ Don’t Do This:
// Deeply nested ifs
if (user != null) {
if (user.isActive) {
if (user.name.isNotEmpty) {
return '...'; // ❌ hard to read
}
}
}
// Nesting ternaries too deep
var x = a ? b : c ? d : e ? f : g; // ❌ unclear
// Non-bool condition
if (name) { } // ❌ Dart requires bool
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
if requires bool | Dart has no truthy/falsy | Use explicit comparison |
| Nested ternary unreadable | More than two levels | Use switch or if/else |
| Guard clause not returning | Missing return | Add return or throw |
| Switch not exhaustive | Missing case for enum | Add all cases or _ |
| Fallthrough expected | Dart 3 does not fall through | Use separate cases |
| Collection-if with else | No else in collection-if | Use ternary instead |
Real-World Examples
1. Simple Boolean Branch
if (isValid) { save(); } else { showError(); }
2. Ternary in Widget Tree
Text(isLoading ? 'Loading...' : 'Done')
3. Guard Clause for Null
void process(String? input) {
if (input == null) return;
print(input.length);
}
4. Guard Clause for Validation
void register(String email, String password) {
if (email.isEmpty) throw ArgumentError('email required');
if (password.length < 8) throw ArgumentError('password too short');
// proceed
}
5. Switch Expression for Enum
var label = switch (status) {
Status.pending => 'Pending',
Status.active => 'Active',
Status.closed => 'Closed',
};
6. Switch with Pattern Guard
var size = switch (value) {
int n when n < 10 => 'small',
int n when n < 100 => 'medium',
int _ => 'large',
_ => 'unknown',
};
7. Collection-If
var items = [
'always',
if (includeExtra) 'extra',
];
8. Nested Ternary with Fallback
var display = name ?? (isGuest ? 'Guest' : 'User');
9. Guard Clause for Async
Future<void> load(String? url) async {
if (url == null) return;
await fetch(url);
}
10. Exhaustive Sealed Class Switch
sealed class Shape {}
class Circle extends Shape {}
class Square extends Shape {}
String describe(Shape shape) => switch (shape) {
Circle() => 'circle',
Square() => 'square',
};
Visual
Statement vs Expression
┌──────────────────────────────────────────────────────────────┐
│ if/else (STATEMENT) ternary (EXPRESSION) │
│ │
│ if (x > 0) { var result = x > 0 │
│ result = 'positive'; ? 'positive' │
│ } else { : 'non-positive'; │
│ result = 'non-positive'; │
│ } │
│ │
│ Cannot appear in an Can appear anywhere a value │
│ expression position. is expected: assignment, │
│ argument, widget tree, return. │
└──────────────────────────────────────────────────────────────┘
Guard Clauses Flatten Nesting
┌──────────────────────────────────────────────────────────────┐
│ NESTED GUARD CLAUSES │
│ │
│ if (user != null) { if (user == null) return; │
│ if (user.isActive) { if (!user.isActive) return; │
│ if (name.isNotEmpty) { if (name.isEmpty) return; │
│ return 'Active: name'; return 'Active: name'; │
│ } │
│ return 'No name'; │
│ } │
│ return 'Inactive'; │
│ } │
│ return 'No user'; │
│ │
│ Main logic indented Main logic at top level. │
│ three levels deep. No nesting. │
└──────────────────────────────────────────────────────────────┘
Switch Expression
┌──────────────────────────────────────────────────────────────┐
│ var label = switch (status) { │
│ Status.pending => 'Pending', │
│ Status.active => 'Active', │
│ Status.closed => 'Closed', │
│ }; │
│ │
│ Exhaustive: all enum values covered. │
│ If a case is missing, it is a compile error. │
│ Returns a value, so it can be assigned. │
└──────────────────────────────────────────────────────────────┘
Collection-If
┌──────────────────────────────────────────────────────────────┐
│ Column( │
│ children: [ │
│ const Header(), │
│ if (isLoggedIn) const UserMenu(), │
│ const Footer(), │
│ ], │
│ ) │
│ │
│ The UserMenu widget is included only when isLoggedIn. │
│ No else branch; the element is either present or omitted. │
│ For both branches, use a ternary. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
if/else | Statement; condition must be bool |
else if | Chain of conditions, evaluated in order |
| Ternary | Expression: condition ? a : b |
| Ternary in Flutter | Widget selection in widget trees |
| Nesting limit | Two levels for ternary |
| Guard clause | Early return for special cases |
| Type promotion | After if (x == null) return;, x is non-null |
switch statement | Multiple cases, no fallthrough in Dart 3 |
switch expression | Produces a value; exhaustive for enums |
| Pattern guard | case int n when n < 0 |
| Collection-if | if (cond) element inside list/set/map |
Key takeaways:
- Dart requires a
boolcondition. There is no truthy or falsy evaluation;if (name)is an error, andif (name != null)orif (name.isNotEmpty)is the correct form. - The ternary is an expression. It produces a value and can appear in assignments, arguments, and widget trees. The
ifstatement cannot. - Guard clauses flatten nesting. By inverting conditions and returning early, the main logic stays at the top level and the function reads linearly.
- Guard clauses work with type promotion. After
if (user == null) return;, the variable is treated as non-nullable for the rest of the function, so no!or?.is needed. - Dart 3’s
switchis both statement and expression. The expression form produces a value, supports pattern matching, and is checked for exhaustiveness when the scrutinee is an enum or sealed type. - Switch does not fall through. Each case is independent, and the compiler enforces exhaustiveness. This removes a common source of bugs from C-style switches.
- Collection-if conditionally includes elements. Inside a list literal,
if (condition) elementincludes the element only when the condition is true. There is noelse; use a ternary for both branches. - Prefer the simplest construct that fits. A guard clause for early exit, a ternary for a simple choice, a switch expression for multiple cases. The construct should match the shape of the decision.
Remember: Control flow determines which code runs. Dart distinguishes statements from expressions, and that distinction determines where each construct can appear. The ternary is an expression and fits in value positions; the if statement does not. Guard clauses keep the happy path at the top level by handling special cases early, and Dart’s type promotion makes them especially effective with nullable values. The switch expression in Dart 3 is exhaustive and pattern-aware, which makes it the right tool for branching on enums and sealed types. The discipline is to choose the construct that fits the decision: a guard clause for invalid input, a ternary for either-or, a switch for multiple cases. When conditionals are structured this way, the code reads as a sequence of clear decisions rather than a nest of intertwined conditions.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!