Watch the video (12 min)
On this page26 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What an Unhandled Exception Does
  5. 2. Catching It with try and catch
  6. 3. What Gets Skipped
  7. 4. catch (e) — The Exception Object
  8. 5. on FormatException catch (e) — Catch One Kind
  9. 6. on Without catch
  10. 7. Several on Clauses
  11. 8. Specific First, General Last
  12. 9. catch (e, s) — The Stack Trace
  13. 10. finally Always Runs
  14. 11. finally Runs Even After return
  15. 12. Catching Too Broadly
  16. 13. tryParse or try/catch?
  17. 14. A Real-World Example
  18. Code Examples
  19. Common Beginner Mistakes
  20. Best Practices
  21. Mini Challenge
  22. Practice Questions
  23. Practice Answers
  24. Quick Revision
  25. Key Takeaways
  26. What's Next?

Lecture 84 — try, catch & finally

Category14 — Error & Exception Handling
Lecture084 of 140
Estimated video length12 min
TargetDart 3.12.2
LevelIntermediate

Until now, one bad piece of text in int.parse ended the whole program. try and catch let 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 from dart analyze, on Dart 3.12.2. Long file paths in stack traces are shortened to file:///.../main.dart.


Prerequisites

Students should already understand, from Lectures 010, 030–042 and 083:

  • int.parse, double.parse, the FormatException they cause, and tryParse (Lecture 010)
  • functions with return (Lectures 030–033)
  • lists, loops, and firstWhere with orElse (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:

dart
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 try and handle failures in catch
  • Catch one kind of exception with on FormatException catch (e)
  • Order several on clauses from most specific to most general
  • Read a stack trace with catch (e, s)
  • Use finally for code that must run whatever happens, even after return
  • Recognize when a catch is too broad, and why catching an Error hides bugs
  • Choose between tryParse and try/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.

dart
void main() {
  print('Start');
  var age = int.parse('twenty');
  print('Age: $age');
  print('End');
}
output
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/catch is 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.

dart
void main() {
  print('Start');
  try {
    var age = int.parse('twenty');
    print('Age: $age');
  } catch (e) {
    print('That was not a number.');
  }
  print('End');
}
output
Start
That was not a number.
End

The 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.

dart
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');
    }
  }
}
output
Parsing 42
Got 42
Parsing oops
Failed on oops

For '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.

dart
void main() {
  try {
    int.parse('twelve');
  } catch (e) {
    print(e.runtimeType);
    print(e);
  }
}
output
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).

dart
void main() {
  try {
    int.parse('12x');
  } on FormatException catch (e) {
    print('Problem: ${e.message}');
    print('Text: ${e.source}');
  }
}
output
Problem: Invalid radix-10 number
Text: 12x

Any 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:

dart
void main() {
  var price = 0.0;
  try {
    price = double.parse('9,99');
  } on FormatException {
    print('Use a dot, not a comma.');
  }
  print('Price: $price');
}
output
Use a dot, not a comma.
Price: 0.0

price 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.

dart
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);
}
output
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:

dart
void main() {
  try {
    int.parse('abc');
  } on Exception {
    print('Something went wrong.');
  } on FormatException {
    print('Not a number.'); // never runs
  }
}
output
Something went wrong.

The program runs, but dart analyze warns:

output
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_subtype

Put 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.

dart
int readAge(String text) {
  return int.parse(text);
}

void main() {
  try {
    readAge('ten');
  } on FormatException catch (e, s) {
    print('Error: ${e.message}');
    print(s);
  }
}
output
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".

dart
void main() {
  for (var text in ['15', 'fifteen']) {
    try {
      print('Quantity: ${int.parse(text)}');
    } on FormatException {
      print('Invalid quantity: $text');
    } finally {
      print('Checked $text');
    }
  }
}
output
Quantity: 15
Checked 15
Invalid quantity: fifteen
Checked fifteen

finally even runs when nothing catches the exception — it runs first, and then the program stops:

dart
void main() {
  try {
    int.parse('oops');
  } finally {
    print('Cleanup');
  }
  print('End');
}
output
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:

dart
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')}');
}
output
try: 42
finally: 42
Result: 42
try: abc
catch: abc
finally: abc
Result: 0

finally 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:

dart
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.');
  }
}
output
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:

CrashKindFix instead of catching
firstWhere with no match (042)StateErrorgive it orElse
byName('huge') (067)ArgumentErroronly call it with names you have checked
Stack Overflow (038)StackOverflowErroradd the missing base case
int.parse('twenty') (010)FormatExceptioncatch it — or use tryParse

The optional lint rule avoid_catching_errors flags on StateError and friends (we set up lint rules in Lecture 117):

output
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_errors

13. tryParse or try/catch?

For one conversion with a fallback, tryParse is shorter and needs no try:

dart
void main() {
  var quantity = int.tryParse('two') ?? 1;
  print('Quantity: $quantity');
}
output
Quantity: 1

try/catch wins when several steps can fail and one handler covers them all, or when there is no try… version of the operation:

dart
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}');
  }
}
output
Bad price: 5O

That 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:

dart
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');
}
output
Deposited 500
Skipped "abc": not a number
Deposited 250
Checked 3 deposits
Balance: 1750

Catch 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

dart
void main() {
  try {
    var year = int.parse('2O26');
    print('Year: $year');
  } catch (e) {
    print('Could not read the year.');
  }
  print('Done');
}
output
Could not read the year.
Done

Example 2 — Reading the FormatException

dart
void main() {
  try {
    int.parse('7 apples');
  } on FormatException catch (e) {
    print(e.message);
    print('Text was: ${e.source}');
  }
}
output
Invalid radix-10 number
Text was: 7 apples

Example 3 — on Without catch

dart
void main() {
  var height = 0.0;
  try {
    height = double.parse('1.75m');
  } on FormatException {
    print('Height must be a plain number.');
  }
  print('Height: $height');
}
output
Height must be a plain number.
Height: 0.0

Example 4 — Two on Clauses

dart
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);
}
output
Size: 38
Not a size
No such position

Example 5 — The Stack Trace

dart
void main() {
  try {
    double.parse('abc');
  } on FormatException catch (e, s) {
    print(e.message);
    print(s.toString().split('\n').first);
  }
}
output
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

dart
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');
}
output
1. try starts
2. catch
3. finally
4. after

Example 7 — Student Marks

dart
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}');
}
output
Ignored: absent
Average: 81.66666666666667

Example 8 — Shopping Total

dart
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');
}
output
Bad price: 1,200
Lines: 3, Total: 349

Example 9 — Bank Withdrawal

dart
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');
}
output
Transaction closed
Invalid amount: 3oo
Transaction closed
Balance: 700

Example 10 — Temperature Readings

dart
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');
}
output
Sensor error: --
Highest: 23.0

Common Beginner Mistakes

1. try on its own

dart
// try {
//   print(int.parse('5'));
// }
output
Error: A try block must be followed by an 'on', 'catch', or 'finally' clause.

2. Using a variable declared inside try after it

dart
// try {
//   var age = int.parse('20');
// } on FormatException {
//   print('Bad age');
// }
// print(age);
output
Error: Undefined name 'age'.

Declare the variable before try and assign it inside, as in section 6.

3. An empty catch

dart
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 try blocks small: wrap only the lines that can fail.
  • Prefer on SomeException over a bare catch (e).
  • Order on clauses from specific to general.
  • Never leave a catch empty; tell the user or record what happened.
  • Put must-run work in finally, not copied into both try and catch.
  • Show users a friendly message; keep the stack trace for programmers.
  • Use tryParse for a single conversion; use try/catch when 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.

output
Bad score: "x"
Bad score: ""
Attempts: 4
Total: 200

Try it yourself before opening the solution.

Show solution
dart
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');
}
output
Bad score: "x"
Bad score: ""
Attempts: 4
Total: 200

An empty string is not a number either, so int.parse('') throws a FormatException.


Practice Questions

Beginner

  1. Parse 'abc' with int.parse inside try. In catch, print Invalid. After the try/catch, print Finished.
  2. Parse '4.5kg' with double.parse, catching only FormatException, and print the exception's message.
  3. Write a try/catch/finally that parses '10' and prints the number, with finally printing Always.

Application

  1. Given ['3', 'three', '7'], print the sum of the valid numbers using try/catch in a loop.

  2. Write int toAge(String text) that returns the parsed number, or -1 on a FormatException. Print toAge('25') and toAge('old').

  3. Using readMark from section 7, what does readMark(['x'], 3) print, and which clause handled it?

  4. Predict the output, then run it:

    dart
    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'));
    }
  5. Rewrite this using tryParse and ?? so no try is needed: var n = 0; try { n = int.parse('5x'); } on FormatException {}

Challenge

  1. Ali's cart holds prices ['300', '45.5', 'free', '120']. Use double.parse. Print each bad entry, then the total and how many entries were valid.
  2. Explain why on FormatException would be better than a bare catch (e) in section 12's loop, and fix the loop's real bug so it prints Total: 245.

Practice Answers

Question 1 — Solution

dart
void main() {
  try {
    int.parse('abc');
  } catch (e) {
    print('Invalid');
  }
  print('Finished');
}
output
Invalid
Finished

Question 2 — Solution

dart
void main() {
  try {
    double.parse('4.5kg');
  } on FormatException catch (e) {
    print(e.message);
  }
}
output
Invalid double

Question 3 — Solution

dart
void main() {
  try {
    print(int.parse('10'));
  } on FormatException {
    print('Invalid');
  } finally {
    print('Always');
  }
}
output
10
Always

Question 4 — Solution

dart
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');
}
output
Skipped three
Sum: 10

Question 5 — Solution

dart
int toAge(String text) {
  try {
    return int.parse(text);
  } on FormatException {
    return -1;
  }
}

void main() {
  print(toAge('25'));
  print(toAge('old'));
}
output
25
-1

Question 6 — Solution

output
There is no entry 3.

marks[3] fails before int.parse ever runs, so the RangeError clause handles it.

Question 7 — Solution

output
checked 1
ok
checked a
bad

finally runs before each result is returned to main.

Question 8 — Solution

dart
void main() {
  var n = int.tryParse('5x') ?? 0;
  print(n);
}
output
0

Question 9 — Solution

dart
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');
}
output
Bad entry: free
Total: 465.5
Valid entries: 3

Question 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 <:

dart
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.');
  }
}
output
Total: 245

Quick Revision

  1. What happens to the lines after a failing line inside try?
  2. Where does the program continue after a catch runs?
  3. What is the difference between catch (e) and on FormatException catch (e)?
  4. Why must the specific on clause come before the general one?
  5. What does the second name in catch (e, s) hold?
  6. Does finally run after a return in try?
  7. Why is catching a StateError usually the wrong fix?
  8. When is tryParse simpler than try/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 the catch.
  • on FormatException catch (e) catches only that type and gives e its fields.
  • Dart checks on clauses top to bottom; put specific types first.
  • catch (e, s) gives the stack trace — read down to your own code.
  • finally always runs: on success, on failure, after return, and even before a crash.
  • Catch exceptions; fix errors. A broad catch can hide bugs behind wrong messages.
  • For one conversion, tryParse is simpler; try/catch covers 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.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.