Debugging Techniques
Why Debugging Matters
Debugging is an essential skill for every Flutter developer. No matter how carefully you write code, bugs will inevitably appear. Knowing how to find and fix them efficiently separates productive developers from frustrated ones. In this lesson, you’ll learn a comprehensive set of debugging techniques that will help you diagnose and resolve issues quickly.
print() and debugPrint()
The simplest debugging technique is printing values to the console. Dart provides two main functions for this purpose.
Using print()
The print() function outputs a string to the console. It works for quick checks but has limitations with long output.
Basic print() Debugging
void main() {
String userName = 'Edrees';
int userAge = 28;
List<String> hobbies = ['coding', 'reading', 'gaming'];
print('User name: \$userName');
print('User age: \$userAge');
print('Hobbies: \$hobbies');
print('Number of hobbies: \${hobbies.length}');
}
Using debugPrint()
The debugPrint() function is preferred in Flutter because it throttles output to avoid dropped lines on Android. It also handles long strings better than print().
debugPrint() for Flutter
import 'package:flutter/foundation.dart';
class UserProfile extends StatelessWidget {
final String name;
final int age;
const UserProfile({super.key, required this.name, required this.age});
@override
Widget build(BuildContext context) {
debugPrint('Building UserProfile: name=\$name, age=\$age');
debugPrint('Context: \${context.widget.runtimeType}');
return Card(
child: Column(
children: [
Text(name),
Text('Age: \$age'),
],
),
);
}
}
debugPrint() instead of print() in Flutter apps. It prevents output from being dropped on Android and provides better formatting for long strings. You can also set debugPrint = (String? message, {int? wrapWidth}) {}; to silence all debug prints in production.Breakpoints in VS Code and Android Studio
Breakpoints allow you to pause your program at specific lines and inspect the state of all variables at that moment. This is far more powerful than print debugging.
Setting Breakpoints in VS Code
In VS Code, click in the gutter (the space to the left of line numbers) to add a red dot breakpoint. When your app reaches that line, execution pauses.
Code with Breakpoint Targets
class CounterScreen extends StatefulWidget {
const CounterScreen({super.key});
@override
State<CounterScreen> createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _counter = 0;
void _incrementCounter() {
// Set a breakpoint on the next line to inspect _counter
setState(() {
_counter++; // <-- Breakpoint here
});
debugPrint('Counter incremented to: \$_counter');
}
@override
Widget build(BuildContext context) {
// Set a breakpoint here to inspect build calls
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: Text(
'Count: \$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: const Icon(Icons.add),
),
);
}
}
Setting Breakpoints in Android Studio
In Android Studio, click in the gutter next to the line number. A red circle appears. You can also right-click a breakpoint to add conditions.
Conditional Breakpoints
You can set breakpoints that only trigger when a condition is true. This is useful when debugging loops or frequently called methods.
Conditional Breakpoint Example
void processItems(List<Map<String, dynamic>> items) {
for (int i = 0; i < items.length; i++) {
var item = items[i];
var price = item['price'] as double;
// Set conditional breakpoint: price < 0
// Right-click breakpoint > Edit > Condition: price < 0
var total = price * (item['quantity'] as int);
debugPrint('Item \$i: price=\$price, total=\$total');
}
}
Stepping Through Code
Once your program hits a breakpoint, you can control execution step by step. There are three main stepping commands:
Step Over (F10)
Step Over executes the current line and moves to the next line in the same function. If the current line contains a function call, it runs the entire function without stepping into it.
Step Into (F11)
Step Into moves execution into the function being called on the current line. Use this when you want to see what happens inside a function.
Step Out (Shift+F11)
Step Out continues execution until the current function returns, then pauses at the calling function. Use this when you’ve seen enough of the current function.
Stepping Example
double calculateDiscount(double price, double percentage) {
// Step Into brings you here
var discount = price * (percentage / 100);
var finalPrice = price - discount;
return finalPrice; // Step Out returns to the caller
}
void processOrder() {
var price = 99.99;
var discount = 15.0;
// Step Over: runs calculateDiscount without entering it
// Step Into: enters calculateDiscount function
var finalPrice = calculateDiscount(price, discount);
// After Step Over or Step Out, execution continues here
debugPrint('Final price: \$finalPrice');
}
Watch Expressions
Watch expressions let you monitor specific variables or expressions as you step through code. They update automatically at each breakpoint or step.
Adding Watch Expressions
In the Debug sidebar (VS Code) or Debug tool window (Android Studio), find the Watch section and click the + button to add expressions.
Useful Watch Expressions
// Given this code paused at a breakpoint:
class ShoppingCart {
List<CartItem> items = [];
double get totalPrice =>
items.fold(0, (sum, item) => sum + item.price * item.quantity);
void addItem(CartItem item) {
items.add(item); // <-- Breakpoint here
}
}
// Useful watch expressions:
// items.length
// totalPrice
// items.last.price
// items.where((i) => i.quantity > 1).toList()
// items.map((i) => i.name).toList()
Evaluate Expressions
The Evaluate Expression feature lets you run any Dart expression while paused at a breakpoint. This is like having a REPL inside your running app.
Evaluate Expression Examples
// While paused at a breakpoint, open Evaluate Expression:
// VS Code: Debug Console (type expressions directly)
// Android Studio: Run > Evaluate Expression (Alt+F8)
// Examples of expressions you can evaluate:
// items.length
// items.where((i) => i.price > 50).toList()
// jsonEncode(items.first.toJson())
// MediaQuery.of(context).size
// Theme.of(context).colorScheme.primary
Debug Console
The Debug Console shows all output from print() and debugPrint() calls, plus any expressions you evaluate. It also shows Flutter framework messages and warnings.
Debug Console Output Examples
// Typical debug console output:
// flutter: Building UserProfile: name=Edrees, age=28
// flutter: Counter incremented to: 1
// flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══
// flutter: The following assertion was thrown building...
// flutter: RenderBox was not laid out: RenderFlex#abc123
// flutter: 'package:flutter/src/rendering/box.dart':
// flutter: Failed assertion: line 1972 pos 12: 'hasSize'
Assert Statements
The assert statement checks a condition during development and throws an error if the condition is false. Asserts are removed in release builds, so they have zero performance impact in production.
Using Assert for Defensive Programming
class BankAccount {
double _balance;
BankAccount(this._balance) {
assert(_balance >= 0, 'Initial balance cannot be negative');
}
void deposit(double amount) {
assert(amount > 0, 'Deposit amount must be positive: \$amount');
_balance += amount;
}
void withdraw(double amount) {
assert(amount > 0, 'Withdrawal amount must be positive: \$amount');
assert(amount <= _balance,
'Insufficient funds: tried \$amount but balance is \$_balance');
_balance -= amount;
}
double get balance => _balance;
}
void main() {
var account = BankAccount(100);
account.deposit(50); // OK
account.withdraw(200); // AssertionError in debug mode!
}
assert for validation that should happen in production. Asserts are completely removed in release builds (flutter build). For production validation, use if statements and throw proper exceptions.Flutter Error Messages (Red Screen of Death)
When Flutter encounters an error during rendering, it displays a bright red error screen (in debug mode) or a gray screen (in release mode). Understanding these error screens is crucial for debugging.
Reading the Red Screen
The red screen shows three important pieces of information:
- Error type: The first line tells you what went wrong (e.g.,
RenderFlex overflowed) - Error message: A detailed description of the problem
- Stack trace: The chain of function calls that led to the error
Common Red Screen Error
// This code causes a RenderFlex overflow error:
class BadLayout extends StatelessWidget {
const BadLayout({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
// This Text widget can overflow the Row
Text(
'This is a very long text that will overflow '
'the horizontal space available in this Row widget '
'and cause a RenderFlex overflowed error',
style: const TextStyle(fontSize: 24),
),
],
);
}
}
// Error message in console:
// ══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞══
// A RenderFlex overflowed by 156 pixels on the right.
// The relevant error-causing widget was:
// Row <-- This tells you which widget caused it
Common Errors and How to Read Stack Traces
Stack traces show the sequence of function calls that led to an error. Reading them from top to bottom shows you the most recent call first.
Reading a Stack Trace
// Example error and stack trace:
// ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞══
// The following _TypeError was thrown building MyWidget:
// type 'Null' is not a subtype of type 'String'
//
// The relevant error-causing widget was:
// MyWidget file:///lib/screens/home.dart:45:15
//
// When the exception was thrown, this was the stack:
// #0 MyWidget.build (package:myapp/widgets/my_widget.dart:23:20)
// #1 StatelessElement.build (package:flutter/src/widgets/framework.dart:5765:28)
// #2 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5694:15)
//
// Look at line #0 -- that's YOUR code where the error happened
// Lines #1 and #2 are Flutter framework code (usually ignore these)
Most Common Flutter Errors
Null Error
// Error: type 'Null' is not a subtype of type 'String'
// Cause: trying to use a null value as a non-nullable type
// Bad:
String? name;
Text(name!); // Crashes if name is null
// Fix:
Text(name ?? 'Unknown'); // Provide a default value
setState() Called After dispose()
// Error: setState() called after dispose()
// Cause: async operation completes after widget is removed
// Bad:
class MyWidget extends StatefulWidget { ... }
class _MyWidgetState extends State<MyWidget> {
void _loadData() async {
var data = await fetchFromApi();
setState(() { // Widget might be disposed!
_data = data;
});
}
// Fix: check mounted before setState
void _loadDataFixed() async {
var data = await fetchFromApi();
if (mounted) {
setState(() {
_data = data;
});
}
}
}
Debugging Layout Overflow Errors
Layout overflow errors are among the most common Flutter issues. They occur when a widget tries to be larger than the space available.
Fixing Common Overflow Errors
// PROBLEM: Row overflow
Row(
children: [
Text('Very long text that overflows...'),
],
)
// FIX 1: Wrap with Expanded
Row(
children: [
Expanded(
child: Text(
'Very long text that now wraps...',
overflow: TextOverflow.ellipsis,
),
),
],
)
// PROBLEM: Column overflow when keyboard appears
Column(
children: [
// Many widgets that overflow when keyboard pushes them up
],
)
// FIX: Wrap with SingleChildScrollView
SingleChildScrollView(
child: Column(
children: [
// Widgets can now scroll
],
),
)
// PROBLEM: Unbounded height in ListView
Column(
children: [
ListView( // ERROR: Vertical viewport given unbounded height
children: [...],
),
],
)
// FIX: Wrap ListView with Expanded
Column(
children: [
Expanded(
child: ListView(
children: [...],
),
),
],
)
p in the terminal while your app is running, or by adding debugPaintSizeEnabled = true; to your code. This shows visual borders around every widget, making it easy to see which widget is causing overflow.Summary
In this lesson, you learned essential debugging techniques for Flutter development:
- Use
debugPrint()overprint()for reliable console output - Set breakpoints in your IDE to pause execution and inspect state
- Step through code line by line (Step Over, Step Into, Step Out)
- Watch expressions and evaluate expressions for real-time inspection
- Use
assertstatements for development-time safety checks - Read Flutter error messages and stack traces to find the root cause
- Debug layout overflow errors with proper widget wrapping
debugPrint() statements and debug-only code before releasing your app. While assert statements are automatically removed, print statements are not and can impact performance.