On this page26 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. The Debugging Mindset
- 2. Reproduce with Small, Known Input
- 3. Reading a Stack Trace
- 4. Print Debugging Done Well
- 5. Print the Type and Show the Edges
- 6. log() from dart:developer
- 7. debugger(): a Breakpoint Written in Code
- 8. assert as a Debugging Aid
- 9. The VS Code Debugger: Breakpoints
- 10. Stepping Through Code
- 11. Variables, Watch and Call Stack
- 12. Conditional Breakpoints and Logpoints
- 13. dart run --observe
- 14. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 115 — Debugging Dart
| Category | 19 — Dart Developer Tools |
| Lecture | 115 of 140 |
| Estimated video length | 12 min |
| Target | Dart 3.12.2 |
| Level | Advanced |
The analyzer catches code that is written wrong. Debugging finds code that runs wrong: it compiles, prints a number, and the number is not the right one.
Contents
Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways
Every output and error message in this lecture is real, produced on Dart 3.12.2. File paths are shortened to
…, and Dart's start-up frames at the end of stack traces are left out. The VS Code steps are described in words; try them yourself.
Prerequisites
Students should already understand, from Lectures 001–114:
- VS Code with the Dart extension and its Run | Debug links (Lecture 002)
- list indexes and
forloops (Lectures 026, 040) - reading a stack trace (Lecture 083) and
assertwith--enable-asserts(Lecture 086) - importing
dart:libraries (Lecture 101) anddart run(Lecture 113)
Connection With Previous Lectures
Run dart analyze (Lecture 114) on this program and it reports "No issues found!":
double averageOfLast(List<int> steps, int days) {
var total = 0;
for (var i = steps.length - days; i < steps.length - 1; i++) {
total += steps[i];
}
return total / days;
}
void main() {
var steps = [4000, 6000, 8000, 10000, 12000];
print('Average of last 3 days: ${averageOfLast(steps, 3)}');
}Average of last 3 days: 6000.0Sara walked 8000, 10000 and 12000 steps on her last three days, so the answer should be
10000.0. Nothing crashed and the analyzer is happy, yet the result is wrong. This lecture
shows how to track the bug down instead of guessing.
Learning Objectives
By the end of this lecture you will be able to:
- Follow the four steps: reproduce, isolate, hypothesise, verify
- Read a stack trace to find the line that failed
- Write debug prints with labels, values and types
- Explain what
log()anddebugger()fromdart:developerdo underdart run - Use
assertto check an assumption while debugging - Set breakpoints and step through code in the VS Code debugger
- Use the Watch, Variables and Call Stack panels, conditional breakpoints and logpoints
- Find and fix an off-by-one bug
1. The Debugging Mindset
A bug is a difference between what the program does and what you expected.
Analogy — debugging is detective work. You do not arrest the first suspect; you collect clues until only one explanation is left.
| Step | Question | For Sara's program |
|---|---|---|
| Reproduce | Can I make it fail every time? | Same five numbers, always 6000.0 |
| Isolate | Where is the smallest failing piece? | averageOfLast, not print |
| Hypothesise | What exact cause would explain it? | "The loop adds the wrong days" |
| Verify | Does a test prove or disprove it? | Print each day the loop adds |
Change one thing at a time, or you won't know which change fixed it.
2. Reproduce with Small, Known Input
Pick input where you can do the arithmetic in your head:
double averageOfLast(List<int> steps, int days) {
var total = 0;
for (var i = steps.length - days; i < steps.length - 1; i++) {
total += steps[i];
}
return total / days;
}
void main() {
print(averageOfLast([1, 2, 3], 3)); // expect 2.0
print(averageOfLast([1, 2, 3], 1)); // expect 3.0
}1.0
0.00.0 for the last day alone is a strong clue: the loop added nothing.
3. Reading a Stack Trace
Suppose Ali "fixes" the loop by guessing i <= steps.length. Now the program crashes:
Unhandled exception:
RangeError (length): Invalid value: Not in inclusive range 0..4: 5
#0 List.[] (dart:core-patch/growable_array.dart)
#1 averageOfLast (file:///…/main.dart:4:19)
#2 main (file:///…/main.dart:11:36)As in Lecture 083, skip Dart's own frame (#0) and start at the first frame in your
file: line 4, column 19 is steps[i]. Index 5 does not exist in a five-item list.
Guessing turned a wrong answer into a crash; that is why we hypothesise and verify.
4. Print Debugging Done Well
A bare print(total) prints 8000 with no context. A good debug print has a label, the
values, and a marker so you can find and delete it later:
void main() {
var steps = [4000, 6000, 8000, 10000, 12000];
var total = 0;
for (var i = 2; i < steps.length - 1; i++) {
print('DEBUG i=$i steps[i]=${steps[i]} total=$total');
total += steps[i];
}
print('DEBUG final total=$total');
}DEBUG i=2 steps[i]=8000 total=0
DEBUG i=3 steps[i]=10000 total=8000
DEBUG final total=18000The loop runs for i = 2 and i = 3 only. Index 4, the 12000-step day, is never added.
5. Print the Type and Show the Edges
Two values can print the same and still be different. Add runtimeType, and wrap text in
brackets so hidden spaces show up:
void main() {
var qty = '3';
var name = 'Ali ';
print('Items: ${qty * 2}');
print('DEBUG qty=$qty (${qty.runtimeType})');
print('DEBUG name=[$name] length=${name.length}');
}Items: 33
DEBUG qty=3 (String)
DEBUG name=[Ali ] length=4qty is the text '3', so * 2 repeats it. And name == 'Ali' would be false because of
the trailing space.
6. log() from dart:developer
dart:developer has a log() function that sends a message, tagged with a name, to a
connected debugger or DevTools instead of the terminal:
import 'dart:developer';
void main() {
print('before');
log('total is 18000', name: 'steps');
print('after');
}before
afterUnder dart run the log line is not shown at all. Started with Debug in VS Code, it
appears in the Debug Console. So log() is for messages you want while debugging, kept
out of the program's normal output.
7. debugger(): a Breakpoint Written in Code
debugger() pauses the program at that line, as if a breakpoint were set there. That is
only useful when a debugger is attached. Under a plain dart run it pauses anyway, with
nothing to resume it:
import 'dart:developer';
void main() {
print('before');
debugger();
print('after');
}beforeafter never appears and the program never ends; press Ctrl + C. (A program compiled with
dart compile exe ignores debugger().) Use debugger(when: condition) to pause only in
one case, and delete every debugger() call when you are done.
8. assert as a Debugging Aid
Your hypothesis is "the loop adds fewer than days values". Turn it into an assert, so
the program stops at the exact moment the assumption breaks:
double averageOfLast(List<int> steps, int days) {
var total = 0;
var added = 0;
for (var i = steps.length - days; i < steps.length - 1; i++) {
total += steps[i];
added++;
}
assert(added == days, 'added $added days, expected $days');
return total / days;
}
void main() {
print(averageOfLast([4000, 6000, 8000, 10000, 12000], 3));
}$ dart run --enable-asserts main.dart
Unhandled exception:
'file:///…/main.dart': Failed assertion: line 8 pos 10: 'added == days': added 2 days, expected 3The hypothesis is verified: one day is missing. Without --enable-asserts, the assert is
skipped and 6000.0 prints as before (Lecture 086).
9. The VS Code Debugger: Breakpoints
Prints make you edit, rerun and delete. A debugger freezes the running program so you can look at everything at once.
- Click in the gutter left of a line number (or press F9). A red dot appears: a breakpoint.
- Click Debug above
main(), or press F5. - The program runs until it reaches the breakpoint, then stops before running that line. The line is highlighted, and the debug toolbar appears at the top.
Put a breakpoint on total += steps[i]; in Sara's program and press F5.
10. Stepping Through Code
While paused, the toolbar moves you forward one piece at a time:
| Button | Key | What it does |
|---|---|---|
| Continue | F5 | Run to the next breakpoint (or the end) |
| Step Over | F10 | Run this line, stop on the next one |
| Step Into | F11 | Go inside the function this line calls |
| Step Out | Shift + F11 | Finish this function, stop where it was called |
| Stop | Shift + F5 | End the program |
On print(averageOfLast(steps, 3)), Step Into enters averageOfLast. Step Over on
the same line runs the whole call and lands on the next line of main. By default, Step
Into skips Dart's own library code and stays in your files.
11. Variables, Watch and Call Stack
The Run and Debug side bar shows three panels while paused:
- Variables — every local variable and its current value:
steps,days,total,i. Expandstepsto see each index. - Watch — click + and type an expression, like
i < steps.length - 1. It is recalculated each time the program pauses. - Call Stack — the same list of frames as a stack trace, but for a live program:
averageOfLaston top,mainbelow. Click a frame to see that function's variables.
Pressing Continue with that watch shows it is true at i = 3 and false once i
reaches 4: the loop condition is the culprit. You can also type an expression into the
Debug Console while paused.
12. Conditional Breakpoints and Logpoints
Stopping on every pass of a 1000-item loop is slow. Right-click the gutter instead:
- Add Conditional Breakpoint… → type
i == 4. The program pauses only when it istrue. If it never pauses, index 4 is never reached: one more clue. - Add Logpoint… → type
i={i} total={total}. Expressions go in{ }. Each time the line runs, the message goes to the Debug Console and the program does not stop.
A logpoint is a debug print that never touches your code, so there is nothing to delete afterwards.
13. dart run --observe
Outside VS Code, --observe starts the program with the Dart VM service, the connection
debuggers and DevTools use:
$ dart run --observe main.dart
before
after
vm-service: isolate(…) 'main' has no debugger attached and is paused at exit. Connect to the Dart VM service to debug.
The Dart VM service is listening on http://127.0.0.1:8181/…/
The Dart DevTools debugger and profiler is available at: http://127.0.0.1:8181/…/devtools/?uri=ws://127.0.0.1:8181/…/wsThe program stays paused at the end so a tool can connect; press Ctrl + C to quit. We will open that DevTools link in Lecture 116 — DevTools.
14. A Real-World Example
Let's close Sara's case with all four steps.
- Reproduce —
[1, 2, 3]withdays = 1gives0.0instead of3.0(section 2). - Isolate — only
averageOfLastis involved;mainjust prints. - Hypothesise — the labelled prints (section 4) and the assert (section 8) both show the loop stops one index early: the last day is never added.
- Verify — the last valid index is
steps.length - 1, so the condition must letireach it.i < steps.length - 1stops before it. The fix isi < steps.length.
double averageOfLast(List<int> steps, int days) {
var total = 0;
for (var i = steps.length - days; i < steps.length; i++) {
total += steps[i];
}
return total / days;
}
void main() {
print(averageOfLast([1, 2, 3], 3)); // expect 2.0
print(averageOfLast([1, 2, 3], 1)); // expect 3.0
var steps = [4000, 6000, 8000, 10000, 12000];
print('Average of last 3 days: ${averageOfLast(steps, 3)}');
}2.0
3.0
Average of last 3 days: 10000.0This is an off-by-one bug: a loop boundary one step too short or too long. Verify with the
small cases and the original input, then remove every DEBUG print.
Code Examples
Example 1 — A Labelled Print
void main() {
var price = 250;
var quantity = 4;
print(price * quantity);
print('DEBUG price=$price quantity=$quantity total=${price * quantity}');
}1000
DEBUG price=250 quantity=4 total=1000Example 2 — Brackets Reveal Spaces
void main() {
var name = 'Sara ';
print(name == 'Sara');
print('DEBUG name=[$name] length=${name.length}');
print(name.trim() == 'Sara');
}false
DEBUG name=[Sara ] length=5
trueExample 3 — Printing the Type
void main() {
var average = 170 / 2;
var count = 170 ~/ 2;
print('DEBUG average=$average (${average.runtimeType})');
print('DEBUG count=$count (${count.runtimeType})');
}DEBUG average=85.0 (double)
DEBUG count=85 (int)Example 4 — Tracing a Loop
void main() {
var marks = [70, 80, 90];
var total = 0;
for (var i = 0; i < marks.length; i++) {
total += marks[i];
print('DEBUG i=$i mark=${marks[i]} total=$total');
}
}DEBUG i=0 mark=70 total=70
DEBUG i=1 mark=80 total=150
DEBUG i=2 mark=90 total=240Example 5 — log() Stays Out of the Terminal
import 'dart:developer';
void main() {
var balance = 1000;
log('balance before withdrawal: $balance', name: 'bank');
balance -= 250;
print('Balance: $balance');
}Balance: 750Run it with Debug in VS Code to see the bank message in the Debug Console.
Example 6 — An Assert That Checks an Assumption
int lastIndex(List<int> values) {
assert(values.isNotEmpty, 'lastIndex() needs at least one value');
return values.length - 1;
}
void main() {
print(lastIndex([5, 6, 7]));
print(lastIndex([]));
}$ dart run --enable-asserts main.dart
2
Unhandled exception:
'file:///…/main.dart': Failed assertion: line 2 pos 10: 'values.isNotEmpty': lastIndex() needs at least one valueWithout the flag, the second line quietly prints -1.
Example 7 — Student Marks: The Boundary Bug
Three of these students reached the pass mark of 50, but the program counts one.
void main() {
var marks = [50, 72, 49, 50];
var passed = 0;
for (var mark in marks) {
print('DEBUG mark=$mark passes=${mark > 50}');
if (mark > 50) passed++;
}
print('Passed: $passed');
}DEBUG mark=50 passes=false
DEBUG mark=72 passes=true
DEBUG mark=49 passes=false
DEBUG mark=50 passes=false
Passed: 1How it works
- The debug line shows
50failing, so the bug is in the comparison. - "At least 50" means
mark >= 50. With that fix,Passed: 3. >versus>=is an off-by-one bug in a comparison instead of a loop.
Example 8 — Shopping Total: The First Item Is Missing
void main() {
var prices = [300, 450, 250];
var total = 0;
for (var i = 1; i < prices.length; i++) {
print('DEBUG adding prices[$i]=${prices[i]}');
total += prices[i];
}
print('Total: $total');
}DEBUG adding prices[1]=450
DEBUG adding prices[2]=250
Total: 700How it works
- Expected
1000; got700, which is300short. - The trace never mentions index
0: the loop starts at1. - Lists start at index
0. Changing tovar i = 0givesTotal: 1000.
Example 9 — Bank Balance: A Total That Resets
void main() {
var deposits = [200, 300, 500];
var balance = 0;
for (var amount in deposits) {
balance = 0;
balance += amount;
print('DEBUG amount=$amount balance=$balance');
}
print('Balance: $balance');
}DEBUG amount=200 balance=200
DEBUG amount=300 balance=300
DEBUG amount=500 balance=500
Balance: 500How it works
- The balance should grow, but each debug line equals the deposit alone.
- Hypothesis: something resets it.
balance = 0;inside the loop does. - Delete that line and the result is
Balance: 1000.
Example 10 — Temperature: Assert a Known Answer
double toFahrenheit(double celsius) => celsius * 5 / 9 + 32;
void main() {
assert(toFahrenheit(0) == 32, 'freezing point');
assert(toFahrenheit(100) == 212, 'boiling: got ${toFahrenheit(100)}');
print(toFahrenheit(25));
}$ dart run --enable-asserts main.dart
Unhandled exception:
'file:///…/main.dart': Failed assertion: line 5 pos 10: 'toFahrenheit(100) == 212': boiling: got 87.55555555555556How it works
0passes, which tells us the+ 32is right.100fails, so the multiplication is wrong: it should be* 9 / 5.- After the fix both asserts pass and
77.0prints.
Common Beginner Mistakes
1. Guessing instead of verifying — changing < to <= "to see if it helps" turned a
wrong answer into a RangeError (section 3).
2. Unlabelled prints — five lines of bare numbers can't be told apart. Label every value.
3. Expecting log() in the terminal — under dart run it prints nothing (section 6).
4. Leaving debugger() in the code
// import 'dart:developer';
//
// void main() {
// debugger(); // ✗ under dart run the program freezes here
// print('Report ready');
// }Nothing is printed and nothing crashes, which makes it confusing. Remove it after debugging.
5. Forgetting --enable-asserts — the debugging assert silently never runs.
Best Practices
- Reproduce with the smallest input whose answer you know.
- Write down the hypothesis before changing code; change one thing at a time.
- Prefix debug prints with
DEBUGand include labels, values and, when in doubt, types. - Prefer breakpoints and logpoints to edits you have to undo.
- Keep
--enable-assertson while developing. - After the fix, remove debug prints and every
debugger()call.
Mini Challenge
Zayan wants his last 3 game scores, but this prints 4. Add a labelled debug print, find the off-by-one, and fix it.
List<int> lastScores(List<int> scores, int n) {
return scores.sublist(scores.length - n - 1);
}
void main() {
print(lastScores([10, 20, 30, 40, 50], 3)); // expect [30, 40, 50]
}[20, 30, 40, 50]Try it yourself before opening the solution.
Show solution
List<int> lastScores(List<int> scores, int n) {
var start = scores.length - n;
print('DEBUG length=${scores.length} n=$n start=$start');
return scores.sublist(start);
}
void main() {
print(lastScores([10, 20, 30, 40, 50], 3)); // expect [30, 40, 50]
}DEBUG length=5 n=3 start=2
[30, 40, 50]How it works
- The buggy start was
5 - 3 - 1, which is1: one index too early. - The last
nitems start atlength - n, here index2. - Delete the
DEBUGline once the output is right.
Practice Questions
Work through these in order — they run from beginner to application to challenge.
Beginner
-
Name the four steps of the debugging mindset, in order.
-
In the trace below, which line of
main.dartshould you look at first?#0 List.[] (dart:core-patch/growable_array.dart) #1 lastMark (file:///…/main.dart:2:15) #2 main (file:///…/main.dart:6:9) -
Rewrite
print(total);as a labelled debug print that also shows the type. -
What does
log('saved', name: 'db')print underdart run?
Application
-
What happens when a plain
dart runreachesdebugger()? -
Find and fix the bug. Expected
Sum: 30.void main() { var values = [5, 10, 15]; var sum = 0; for (var i = 0; i < values.length - 1; i++) { sum += values[i]; } print('Sum: $sum'); } -
You are paused on
var avg = average(marks);. Which button takes you insideaverage, and which brings you back to this line's function? -
Add an
asserttodouble average(List<int> marks)that fails on an empty list, and show the output foraverage([])with--enable-asserts.
Challenge
-
A loop runs 1000 times and breaks only when
iis742. What breakpoint would you set, and what would you type? -
countWarmshould count temperatures from 25 to 30 inclusive. It prints1for[25, 27, 30, 31]. Find the bug with a debug print and fix it.int countWarm(List<int> temps) { var count = 0; for (var t in temps) { if (t > 25 && t < 30) count++; } return count; } void main() { print(countWarm([25, 27, 30, 31])); }
Practice Answers
Question 1 — Solution
Reproduce, isolate, hypothesise, verify.
Question 2 — Solution
Line 2 (#1 lastMark … main.dart:2:16): the first frame in your own file. #0 is Dart's
code.
Question 3 — Solution
void main() {
var total = 240;
print('DEBUG total=$total (${total.runtimeType})');
}DEBUG total=240 (int)Question 4 — Solution
Nothing. The message goes to a connected debugger, such as VS Code's Debug Console.
Question 5 — Solution
The program pauses there and waits for a debugger that is not attached. The next line never runs; quit with Ctrl + C.
Question 6 — Solution
The loop stops before the last index. Use i < values.length:
void main() {
var values = [5, 10, 15];
var sum = 0;
for (var i = 0; i < values.length; i++) {
sum += values[i];
}
print('Sum: $sum');
}Sum: 30Question 7 — Solution
Step Into (F11) enters average. Step Out (Shift + F11) finishes it and returns to
the calling line.
Question 8 — Solution
double average(List<int> marks) {
assert(marks.isNotEmpty, 'average() needs at least one mark');
var total = 0;
for (var mark in marks) {
total += mark;
}
return total / marks.length;
}
void main() {
print(average([80, 90]));
print(average([]));
}$ dart run --enable-asserts main.dart
85.0
Unhandled exception:
'file:///…/main.dart': Failed assertion: line 2 pos 10: 'marks.isNotEmpty': average() needs at least one markQuestion 9 — Solution
A conditional breakpoint inside the loop (right-click the gutter → Add Conditional
Breakpoint…) with the expression i == 742.
Question 10 — Solution
int countWarm(List<int> temps) {
var count = 0;
for (var t in temps) {
print('DEBUG t=$t warm=${t >= 25 && t <= 30}');
if (t >= 25 && t <= 30) count++;
}
return count;
}
void main() {
print(countWarm([25, 27, 30, 31]));
}DEBUG t=25 warm=true
DEBUG t=27 warm=true
DEBUG t=30 warm=true
DEBUG t=31 warm=false
3"Inclusive" means >= and <=. With > and <, the edges 25 and 30 were left out.
Remove the debug line afterwards.
Quick Revision
- What are the four steps of debugging?
- Which stack-trace frame do you read first?
- What three things make a good debug print?
- Where does
log()output go? - Why must you remove
debugger()calls? - What is the difference between Step Over and Step Into?
- How does a logpoint differ from a breakpoint?
- What is an off-by-one bug?
Key Takeaways
- Debug in four steps: reproduce, isolate, hypothesise, verify. Change one thing at a time.
- A stack trace's first frame in your own file is where to look.
- Good debug prints have a label, the value, and sometimes
runtimeTypeor[brackets]. log()writes to a debugger, not the terminal;debugger()pauses, and freezes a plaindart run.- An
assertturns a hypothesis into a check (run with--enable-asserts). - VS Code: breakpoints, Step Over/Into/Out, Variables, Watch, Call Stack, conditional breakpoints and logpoints.
- Off-by-one bugs hide in loop bounds,
subliststarts and>vs>=.
What's Next?
Lecture 116 — DevTools. dart run --observe printed a DevTools link. Next we open it to
see a running program's memory, performance and logs, and find problems that a breakpoint
alone cannot show.