Flutter Setup & First App

Debugging Techniques

50 min Lesson 9 of 12

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.

Note: Flutter provides some of the best debugging tools in mobile development. The combination of hot reload, rich error messages, and integrated IDE debuggers makes Flutter exceptionally developer-friendly.

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'),
        ],
      ),
    );
  }
}
Tip: Use 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.

Note: To start debugging, use Run > Debug (or press F5 in VS Code / Shift+F9 in Android Studio) instead of the normal Run command. The app must be running in debug mode for breakpoints to work.

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
Tip: In VS Code, the Debug Console at the bottom of the screen acts as an interactive REPL. You can type any Dart expression while paused and see the result immediately. This is one of the most powerful debugging features available.

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!
}
Warning: Never use 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: [...],
      ),
    ),
  ],
)
Tip: Enable the Debug Paint overlay by pressing 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.
Note: The Flutter DevTools suite provides a Widget Inspector that lets you visually explore the widget tree and see the size constraints of every widget. Access it from VS Code’s command palette: Flutter: Open DevTools.

Summary

In this lesson, you learned essential debugging techniques for Flutter development:

  • Use debugPrint() over print() 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 assert statements 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
Warning: Remove or disable all debugPrint() statements and debug-only code before releasing your app. While assert statements are automatically removed, print statements are not and can impact performance.