On this page26 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. What an Unhandled Exception Does
- 2. Catching It with try and catch
- 3. What Gets Skipped
- 4. catch (e) — The Exception Object
- 5. on FormatException catch (e) — Catch One Kind
- 6. on Without catch
- 7. Several on Clauses
- 8. Specific First, General Last
- 9. catch (e, s) — The Stack Trace
- 10. finally Always Runs
- 11. finally Runs Even After return
- 12. Catching Too Broadly
- 13. tryParse or try/catch?
- 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 84 — try, catch & finally
| Category | 14 — Error & Exception Handling |
| Lecture | 084 of 140 |
| Estimated video length | 12 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
Until now, one bad piece of text in
int.parseended the whole program.tryandcatchlet the program notice the failure, deal with it, and carry on.
Contents
Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways
Every error and warning in this lecture is real. Errors come from
dart run, warnings fromdart analyze, on Dart 3.12.2. Long file paths in stack traces are shortened tofile:///.../main.dart.
Prerequisites
Students should already understand, from Lectures 010, 030–042 and 083:
int.parse,double.parse, theFormatExceptionthey cause, andtryParse(Lecture 010)- functions with
return(Lectures 030–033) - lists, loops, and
firstWherewithorElse(Lectures 026–028, 042) - the difference between an exception (expected trouble from outside) and an error (a bug in the code) (Lecture 083)
Connection With Previous Lectures
Several lectures ended a story with "the program stops": int.parse('twenty') (Lecture 010),
recursion without a base case (Lecture 038), firstWhere with no match (Lecture 042), and
byName with an unknown name (Lecture 067). Each time we promised to come back.
Here is the problem. Sara's bank app reads three deposits typed by a user:
var deposits = ['500', 'abc', '250'];One typo in the middle, and int.parse('abc') stops everything — the good 250 is never
added and no balance is printed. We need a way to say: try this, and if it fails, do
something sensible instead of crashing.
Learning Objectives
By the end of this lecture you will be able to:
- Explain what an unhandled exception does to a running program
- Wrap risky code in
tryand handle failures incatch - Catch one kind of exception with
on FormatException catch (e) - Order several
onclauses from most specific to most general - Read a stack trace with
catch (e, s) - Use
finallyfor code that must run whatever happens, even afterreturn - Recognize when a catch is too broad, and why catching an
Errorhides bugs - Choose between
tryParseandtry/catch
1. What an Unhandled Exception Does
When an exception is not handled, Dart stops the program on the spot. Nothing after the failing line runs.
void main() {
print('Start');
var age = int.parse('twenty');
print('Age: $age');
print('End');
}Start
Unhandled exception:
FormatException: Invalid radix-10 number (at character 1)
twenty
^
#0 int._handleFormatError (dart:core-patch/integers_patch.dart:150:5)
...Start printed; Age and End never did. The lines starting with # are a stack
trace — section 9 explains them.
2. Catching It with try and catch
Put the risky code inside try { }. If it throws an exception, Dart jumps to catch { }
instead of stopping.
Analogy —
try/catchis a safety net under a tightrope walker. On a good day the net is never touched. On a bad day it catches the fall, and the show goes on.
void main() {
print('Start');
try {
var age = int.parse('twenty');
print('Age: $age');
} catch (e) {
print('That was not a number.');
}
print('End');
}Start
That was not a number.
EndThe program continues after the catch. End is printed this time.
3. What Gets Skipped
As soon as a line in try fails, the rest of try is skipped. When nothing fails, catch
is skipped.
void main() {
for (var text in ['42', 'oops']) {
try {
print('Parsing $text');
var number = int.parse(text);
print('Got $number');
} catch (e) {
print('Failed on $text');
}
}
}Parsing 42
Got 42
Parsing oops
Failed on oopsFor '42', catch never ran. For 'oops', Got … never ran.
4. catch (e) — The Exception Object
The name in catch (e) holds the exception object describing what went wrong. Its type is
Object, because catch (e) accepts anything.
void main() {
try {
int.parse('twelve');
} catch (e) {
print(e.runtimeType);
print(e);
}
}FormatException
FormatException: Invalid radix-10 number (at character 1)
twelve
^
Printing e shows the same message you would have seen in the crash — but now the program
decides what to do with it.
5. on FormatException catch (e) — Catch One Kind
on Type means "only catch exceptions of this type". Now e has that type, so you can read
its fields, such as message (what went wrong) and source (the text being read).
void main() {
try {
int.parse('12x');
} on FormatException catch (e) {
print('Problem: ${e.message}');
print('Text: ${e.source}');
}
}Problem: Invalid radix-10 number
Text: 12xAny other kind of failure is not caught here — it still stops the program. That is a good thing, as section 12 shows.
6. on Without catch
If you don't need the object, drop catch (e) and keep only on Type:
void main() {
var price = 0.0;
try {
price = double.parse('9,99');
} on FormatException {
print('Use a dot, not a comma.');
}
print('Price: $price');
}Use a dot, not a comma.
Price: 0.0price is declared before try, so it can be used after it. A variable declared inside
{ } of try exists only there.
7. Several on Clauses
A try can have several on clauses. Dart checks them top to bottom and runs only the
first one whose type matches.
void readMark(List<String> marks, int index) {
try {
var mark = int.parse(marks[index]);
print('Mark: $mark');
} on FormatException {
print('Entry $index is not a number.');
} on RangeError {
print('There is no entry $index.');
}
}
void main() {
var marks = ['80', 'A+', '90'];
readMark(marks, 0);
readMark(marks, 1);
readMark(marks, 5);
}Mark: 80
Entry 1 is not a number.
There is no entry 5.RangeError is an error — a bug — and section 12 shows a better fix for it. It is here only
so we can see two different clauses fire.
8. Specific First, General Last
FormatException is a kind of Exception. If the general on Exception comes first, it
catches everything and the specific clause below it can never run:
void main() {
try {
int.parse('abc');
} on Exception {
print('Something went wrong.');
} on FormatException {
print('Not a number.'); // never runs
}
}Something went wrong.The program runs, but dart analyze warns:
warning - Dead code: This on-catch block won't be executed because 'FormatException' is a
subtype of 'Exception' and hence will have been caught already. Try reordering the catch
clauses so that this block can be reached, or removing the unreachable catch clause. -
dead_code_on_catch_subtypePut the most specific type first and the most general last.
9. catch (e, s) — The Stack Trace
A second name in catch receives the stack trace: the list of function calls that were
running when the exception happened, newest first.
int readAge(String text) {
return int.parse(text);
}
void main() {
try {
readAge('ten');
} on FormatException catch (e, s) {
print('Error: ${e.message}');
print(s);
}
}Error: Invalid radix-10 number
#0 int._handleFormatError (dart:core-patch/integers_patch.dart:150:5)
#1 int._parseRadix (dart:core-patch/integers_patch.dart:179:14)
#2 int._parse (dart:core-patch/integers_patch.dart:121:12)
#3 int.parse (dart:core-patch/integers_patch.dart:81:12)
#4 readAge (file:///.../main.dart:2:14)
#5 main (file:///.../main.dart:7:5)
...Read it from the top until you reach your own code: readAge, line 2, called from
main, line 7. The dart:core-patch lines are inside Dart itself. The trace is for
programmers; users should see a friendly message instead.
10. finally Always Runs
A finally block runs after try (and catch, if one ran) — whether or not anything
failed. It is the place for work that must always happen, such as "close the receipt" or
"count this attempt".
void main() {
for (var text in ['15', 'fifteen']) {
try {
print('Quantity: ${int.parse(text)}');
} on FormatException {
print('Invalid quantity: $text');
} finally {
print('Checked $text');
}
}
}Quantity: 15
Checked 15
Invalid quantity: fifteen
Checked fifteenfinally even runs when nothing catches the exception — it runs first, and then the
program stops:
void main() {
try {
int.parse('oops');
} finally {
print('Cleanup');
}
print('End');
}Cleanup
Unhandled exception:
FormatException: Invalid radix-10 number (at character 1)
oops
^
...11. finally Runs Even After return
A return inside try or catch does not skip finally. Dart remembers the value, runs
finally, and then leaves the function. Watch the order:
int parseOrZero(String text) {
try {
print('try: $text');
return int.parse(text);
} on FormatException {
print('catch: $text');
return 0;
} finally {
print('finally: $text');
}
}
void main() {
print('Result: ${parseOrZero('42')}');
print('Result: ${parseOrZero('abc')}');
}try: 42
finally: 42
Result: 42
try: abc
catch: abc
finally: abc
Result: 0finally printed before main received the result — in both cases.
12. Catching Too Broadly
A bare catch (e) catches everything, including bugs. This loop has a bug (<= should be
<), but the message blames the user:
void main() {
var marks = ['80', '75', '90'];
try {
var total = 0;
for (var i = 0; i <= marks.length; i++) {
total += int.parse(marks[i]);
}
print('Total: $total');
} catch (e) {
print('Please enter valid numbers.');
}
}Please enter valid numbers.The marks are fine; marks[3] threw a RangeError. With on FormatException instead, the
program would crash loudly and point straight at the bug — which is what you want.
Lecture 083 drew the line: an exception is trouble from outside (bad input), an error is a mistake in the code. Catch exceptions; fix errors. That settles our old promises:
| Crash | Kind | Fix instead of catching |
|---|---|---|
firstWhere with no match (042) | StateError | give it orElse |
byName('huge') (067) | ArgumentError | only call it with names you have checked |
| Stack Overflow (038) | StackOverflowError | add the missing base case |
int.parse('twenty') (010) | FormatException | catch it — or use tryParse |
The optional lint rule avoid_catching_errors flags on StateError and friends (we set up
lint rules in Lecture 117):
info - The type 'StateError' should not be caught because it is a subclass of 'Error'. Try
removing the catch or catching an 'Exception' instead. - avoid_catching_errors13. tryParse or try/catch?
For one conversion with a fallback, tryParse is shorter and needs no try:
void main() {
var quantity = int.tryParse('two') ?? 1;
print('Quantity: $quantity');
}Quantity: 1try/catch wins when several steps can fail and one handler covers them all, or when
there is no try… version of the operation:
void main() {
try {
var total = int.parse('120') + int.parse('80') + int.parse('5O');
print('Total: $total');
} on FormatException catch (e) {
print('Bad price: ${e.source}');
}
}Bad price: 5OThat last price is 5 and the letter O. With tryParse you would need three null checks.
14. A Real-World Example
Sara's deposits again — every good deposit counts, every bad one is reported, and every line is counted:
void main() {
var deposits = ['500', 'abc', '250'];
var balance = 1000;
var checked = 0;
for (var text in deposits) {
try {
var amount = int.parse(text);
balance += amount;
print('Deposited $amount');
} on FormatException catch (e) {
print('Skipped "${e.source}": not a number');
} finally {
checked++;
}
}
print('Checked $checked deposits');
print('Balance: $balance');
}Deposited 500
Skipped "abc": not a number
Deposited 250
Checked 3 deposits
Balance: 1750Catch only the failure you expect, report it clearly, put must-run work in finally, and let
the program carry on.
Code Examples
Example 1 — Basic try/catch
void main() {
try {
var year = int.parse('2O26');
print('Year: $year');
} catch (e) {
print('Could not read the year.');
}
print('Done');
}Could not read the year.
DoneExample 2 — Reading the FormatException
void main() {
try {
int.parse('7 apples');
} on FormatException catch (e) {
print(e.message);
print('Text was: ${e.source}');
}
}Invalid radix-10 number
Text was: 7 applesExample 3 — on Without catch
void main() {
var height = 0.0;
try {
height = double.parse('1.75m');
} on FormatException {
print('Height must be a plain number.');
}
print('Height: $height');
}Height must be a plain number.
Height: 0.0Example 4 — Two on Clauses
void pick(List<String> sizes, int index) {
try {
print('Size: ${int.parse(sizes[index])}');
} on FormatException {
print('Not a size');
} on RangeError {
print('No such position');
}
}
void main() {
pick(['38', 'L'], 0);
pick(['38', 'L'], 1);
pick(['38', 'L'], 2);
}Size: 38
Not a size
No such positionExample 5 — The Stack Trace
void main() {
try {
double.parse('abc');
} on FormatException catch (e, s) {
print(e.message);
print(s.toString().split('\n').first);
}
}Invalid double
#0 double.parse (dart:core-patch/double_patch.dart:111:7)split('\n').first keeps only the top line of the trace.
Example 6 — The Order of try, catch, finally
void main() {
try {
print('1. try starts');
int.parse('x');
print('never printed');
} on FormatException {
print('2. catch');
} finally {
print('3. finally');
}
print('4. after');
}1. try starts
2. catch
3. finally
4. afterExample 7 — Student Marks
void main() {
var marks = ['80', 'absent', '75', '90'];
var total = 0;
var counted = 0;
for (var text in marks) {
try {
total += int.parse(text);
counted++;
} on FormatException {
print('Ignored: $text');
}
}
print('Average: ${total / counted}');
}Ignored: absent
Average: 81.66666666666667Example 8 — Shopping Total
void main() {
var prices = ['250', '1,200', '99'];
var total = 0;
var lines = 0;
for (var text in prices) {
try {
total += int.parse(text);
} on FormatException {
print('Bad price: $text');
} finally {
lines++;
}
}
print('Lines: $lines, Total: $total');
}Bad price: 1,200
Lines: 3, Total: 349Example 9 — Bank Withdrawal
int withdraw(int balance, String text) {
try {
return balance - int.parse(text);
} on FormatException {
print('Invalid amount: $text');
return balance;
} finally {
print('Transaction closed');
}
}
void main() {
var balance = withdraw(1000, '300');
balance = withdraw(balance, '3oo');
print('Balance: $balance');
}Transaction closed
Invalid amount: 3oo
Transaction closed
Balance: 700Example 10 — Temperature Readings
void main() {
var readings = ['21.5', '--', '23.0'];
var highest = -100.0;
for (var text in readings) {
try {
var value = double.parse(text);
if (value > highest) highest = value;
} on FormatException {
print('Sensor error: $text');
}
}
print('Highest: $highest');
}Sensor error: --
Highest: 23.0Common Beginner Mistakes
1. try on its own
// try {
// print(int.parse('5'));
// }Error: A try block must be followed by an 'on', 'catch', or 'finally' clause.2. Using a variable declared inside try after it
// try {
// var age = int.parse('20');
// } on FormatException {
// print('Bad age');
// }
// print(age);Error: Undefined name 'age'.Declare the variable before try and assign it inside, as in section 6.
3. An empty catch
try {
int.parse('x');
} catch (e) {}dart analyze reports info - Empty catch block. … - empty_catches. The failure vanishes
silently. At least print a message.
4. General clause before specific — on Exception above on FormatException makes the
second clause dead code (section 8).
5. Catching errors instead of fixing bugs — a RangeError, StateError or Stack
Overflow means the code is wrong. Fix the code (section 12).
6. Expecting the rest of try to run — after the failing line, everything else in try
is skipped.
Best Practices
- Keep
tryblocks small: wrap only the lines that can fail. - Prefer
on SomeExceptionover a barecatch (e). - Order
onclauses from specific to general. - Never leave a
catchempty; tell the user or record what happened. - Put must-run work in
finally, not copied into bothtryandcatch. - Show users a friendly message; keep the stack trace for programmers.
- Use
tryParsefor a single conversion; usetry/catchwhen several steps share one handler.
Mini Challenge
Zayan's game stores scores as text: ['120', 'x', '80', '']. Add up the valid scores, print
Bad score: "…" for each invalid one, and print the total. Use finally to count attempts.
Bad score: "x"
Bad score: ""
Attempts: 4
Total: 200Try it yourself before opening the solution.
Show solution
void main() {
var scores = ['120', 'x', '80', ''];
var total = 0;
var attempts = 0;
for (var text in scores) {
try {
total += int.parse(text);
} on FormatException {
print('Bad score: "$text"');
} finally {
attempts++;
}
}
print('Attempts: $attempts');
print('Total: $total');
}Bad score: "x"
Bad score: ""
Attempts: 4
Total: 200An empty string is not a number either, so int.parse('') throws a FormatException.
Practice Questions
Beginner
- Parse
'abc'withint.parseinsidetry. Incatch, printInvalid. After thetry/catch, printFinished. - Parse
'4.5kg'withdouble.parse, catching onlyFormatException, and print the exception'smessage. - Write a
try/catch/finallythat parses'10'and prints the number, withfinallyprintingAlways.
Application
-
Given
['3', 'three', '7'], print the sum of the valid numbers usingtry/catchin a loop. -
Write
int toAge(String text)that returns the parsed number, or-1on aFormatException. PrinttoAge('25')andtoAge('old'). -
Using
readMarkfrom section 7, what doesreadMark(['x'], 3)print, and which clause handled it? -
Predict the output, then run it:
String check(String text) { try { int.parse(text); return 'ok'; } on FormatException { return 'bad'; } finally { print('checked $text'); } } void main() { print(check('1')); print(check('a')); } -
Rewrite this using
tryParseand??so notryis needed:var n = 0; try { n = int.parse('5x'); } on FormatException {}
Challenge
- Ali's cart holds prices
['300', '45.5', 'free', '120']. Usedouble.parse. Print each bad entry, then the total and how many entries were valid. - Explain why
on FormatExceptionwould be better than a barecatch (e)in section 12's loop, and fix the loop's real bug so it printsTotal: 245.
Practice Answers
Question 1 — Solution
void main() {
try {
int.parse('abc');
} catch (e) {
print('Invalid');
}
print('Finished');
}Invalid
FinishedQuestion 2 — Solution
void main() {
try {
double.parse('4.5kg');
} on FormatException catch (e) {
print(e.message);
}
}Invalid doubleQuestion 3 — Solution
void main() {
try {
print(int.parse('10'));
} on FormatException {
print('Invalid');
} finally {
print('Always');
}
}10
AlwaysQuestion 4 — Solution
void main() {
var sum = 0;
for (var text in ['3', 'three', '7']) {
try {
sum += int.parse(text);
} on FormatException {
print('Skipped $text');
}
}
print('Sum: $sum');
}Skipped three
Sum: 10Question 5 — Solution
int toAge(String text) {
try {
return int.parse(text);
} on FormatException {
return -1;
}
}
void main() {
print(toAge('25'));
print(toAge('old'));
}25
-1Question 6 — Solution
There is no entry 3.marks[3] fails before int.parse ever runs, so the RangeError clause handles it.
Question 7 — Solution
checked 1
ok
checked a
badfinally runs before each result is returned to main.
Question 8 — Solution
void main() {
var n = int.tryParse('5x') ?? 0;
print(n);
}0Question 9 — Solution
void main() {
var cart = ['300', '45.5', 'free', '120'];
var total = 0.0;
var valid = 0;
for (var text in cart) {
try {
total += double.parse(text);
valid++;
} on FormatException {
print('Bad entry: $text');
}
}
print('Total: $total');
print('Valid entries: $valid');
}Bad entry: free
Total: 465.5
Valid entries: 3Question 10 — Solution
The bare catch hid a RangeError (a bug) behind a message about bad input. With
on FormatException, the RangeError would crash and reveal the bug. The fix is <:
void main() {
var marks = ['80', '75', '90'];
try {
var total = 0;
for (var i = 0; i < marks.length; i++) {
total += int.parse(marks[i]);
}
print('Total: $total');
} on FormatException {
print('Please enter valid numbers.');
}
}Total: 245Quick Revision
- What happens to the lines after a failing line inside
try? - Where does the program continue after a
catchruns? - What is the difference between
catch (e)andon FormatException catch (e)? - Why must the specific
onclause come before the general one? - What does the second name in
catch (e, s)hold? - Does
finallyrun after areturnintry? - Why is catching a
StateErrorusually the wrong fix? - When is
tryParsesimpler thantry/catch?
Key Takeaways
- An unhandled exception stops the program; nothing after the failing line runs.
try { } catch (e) { }handles the failure, and the program continues after thecatch.on FormatException catch (e)catches only that type and giveseits fields.- Dart checks
onclauses top to bottom; put specific types first. catch (e, s)gives the stack trace — read down to your own code.finallyalways runs: on success, on failure, afterreturn, and even before a crash.- Catch exceptions; fix errors. A broad
catchcan hide bugs behind wrong messages. - For one conversion,
tryParseis simpler;try/catchcovers several steps at once.
What's Next?
Lecture 085 — throw & Custom Exceptions. So far every exception came from Dart's own
code, like int.parse. Next, you will throw exceptions from your own functions — for
example a withdrawal larger than the balance — and create custom exception classes that the
on clauses from this lecture can catch.