Watch the video (8 min)
On this page20 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. Compile-Time vs Runtime Errors
  5. 2. What Happens When a Program Crashes
  6. 3. Reading a Stack Trace
  7. 4. Two Families: Error and Exception
  8. 5. The Error Family: Programmer Bugs
  9. 6. The Exception Family: Expected Failures
  10. 7. Fix Errors, Handle Exceptions
  11. 8. A Real-World Example
  12. Code Examples
  13. Common Beginner Mistakes
  14. Best Practices
  15. Mini Challenge
  16. Practice Questions
  17. Practice Answers
  18. Quick Revision
  19. Key Takeaways
  20. What's Next?

Lecture 83 — Errors vs Exceptions

Category14 — Error & Exception Handling
Lecture083 of 140
Estimated video length8 min
TargetDart 3.12.2
LevelIntermediate

Since Lecture 006 we have cut off the lines Dart prints under Unhandled exception:. Today we read them — and learn that every crash is either a bug to fix or a failure to plan for.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every error message and stack trace in this lecture is real, produced with dart run on Dart 3.12.2. Only the folder part of file paths is replaced with <your-folder>.


Prerequisites

Students should already understand, from Lectures 001–082:

  • the runtime crashes met so far: RangeError, Bad state, failed casts, FormatException, Stack Overflow (Lectures 010, 011, 038, 040, 042)
  • int.tryParse and ?? (Lectures 010, 014)
  • functions calling functions (Lectures 030–038)
  • classes, inheritance, is, and Object (Lectures 011, 053–065)
  • object patterns in a switch (Lectures 080–081)

Connection With Previous Lectures

Here are two crashes we have already seen:

output
Unhandled exception:
RangeError (length): Invalid value: Not in inclusive range 0..2: 3
output
Unhandled exception:
FormatException: Invalid radix-10 number (at character 1)
twenty

The first came from a loop that read one index too far — our mistake. The second came from int.parse('twenty') — the code was fine, the input was bad. Both stop the program with the same words, yet one needs a code change and the other cannot be prevented by any code change. Dart separates the two, and so should we.


Learning Objectives

By the end of this lecture you will be able to:

  • Distinguish compile-time errors from runtime crashes
  • Describe what Dart does when a problem is not handled
  • Read a stack trace line by line and find the line to fix
  • Explain the difference between Dart's Error and Exception families
  • Name the common Error types: RangeError, TypeError, StateError, ArgumentError, NoSuchMethodError, StackOverflowError
  • Name the common Exception types: FormatException and I/O exceptions
  • Decide whether a crash should be fixed or handled

1. Compile-Time vs Runtime Errors

A compile-time error is found before the program starts. Nothing runs — not even the lines above the mistake:

dart
void main() {
  print('Start');
  // int count = 'five'; // ✗
}
output
main.dart:3:15: Error: A value of type 'String' can't be assigned to a variable of type 'int'.
  int count = 'five';
              ^

A runtime error happens while the program runs. Everything before it works; then it stops:

dart
void main() {
  var marks = [85, 92];
  print('Start');
  print(marks[5]);
  print('End');
}
output
Start
Unhandled exception:
RangeError (length): Invalid value: Not in inclusive range 0..1: 5

End never prints. The rest of this category is about runtime problems.


2. What Happens When a Program Crashes

When something goes wrong at runtime, Dart throws an object that describes the problem. If no code deals with it, Dart:

  1. prints Unhandled exception:,
  2. prints the object's message (RangeError (length): …),
  3. prints a stack trace — the list of function calls that were running,
  4. stops the program.

Analogy — a stack trace is like a trail of breadcrumbs. It shows every function the program walked through to reach the spot where it fell.

The words "Unhandled exception" appear for every crash, whatever its family. "Unhandled" is the important word: nothing caught the problem.


3. Reading a Stack Trace

This program has the <= bug from Lecture 040, buried two functions deep:

dart
double average(List<int> marks) {
  var total = 0;
  for (var i = 0; i <= marks.length; i++) {
    total += marks[i];
  }
  return total / marks.length;
}

void printReport(String name, List<int> marks) {
  print('Report for $name');
  print('Average: ${average(marks)}');
}

void main() {
  printReport('Ali', [85, 92, 78]);
  print('Done');
}
output
Report for Ali
Unhandled exception:
RangeError (length): Invalid value: Not in inclusive range 0..2: 3
#0      List.[] (dart:core-patch/growable_array.dart)
#1      average (file:///<your-folder>/main.dart:4:19)
#2      printReport (file:///<your-folder>/main.dart:11:21)
#3      main (file:///<your-folder>/main.dart:15:3)
#4      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:313:19)
#5      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)

Each # line is a frame: one function call. Each frame shows the function's name, the file, and line:column.

LineMeaning
#0 List.[]The crash happened inside the list's [] operator — Dart's own code (dart:core).
#1 average … main.dart:4:19[] was called by average, line 4, column 19: marks[i].
#2 printReport … 11:21average was called from printReport, line 11.
#3 main … 15:3printReport was called from main, line 15.
#4, #5Dart's start-up code that called main. Ignore them.

Read top to bottom to go from the crash back to main. Read bottom to top to replay the program: main → printReport → average → []. The line to fix is usually the first frame in your own file — here main.dart:4, inside the loop whose <= should be <.


4. Two Families: Error and Exception

Every thrown object belongs to a class. In Dart's core library, most of them belong to one of two families:

  • Error — a bug in the program. The code did something it never should.
  • Exception — an expected failure. The code was right, but the world was not: bad input, a missing file, a dropped connection.

You can create these objects without throwing them and check their family with is:

dart
void main() {
  Object bug = RangeError('index too big');
  Object failure = FormatException('not a number');
  print(bug is Error);
  print(bug is Exception);
  print(failure is Exception);
  print(failure is Error);
}
output
true
false
true
false

An object belongs to one family, not both. The one leftover is the IntegerDivisionByZeroException from Lecture 017 (10 ~/ 0), which counts as both and is now deprecated. Creating and throwing your own is Lecture 085.


5. The Error Family: Programmer Bugs

ClassMessage starts withTypical cause
RangeErrorRangeErrorindex or position outside the valid range
TypeErrortype 'X' is not a subtype of type 'Y'a failed as cast
StateErrorBad statefirst, single, reduce on an empty list
ArgumentErrorInvalid argument(s)a function got a value it cannot accept
NoSuchMethodErrorNoSuchMethodErrorcalling a missing method on a dynamic value
StackOverflowErrorStack Overflowrecursion without a base case (Lecture 038)

RangeError is a special kind of ArgumentError — it extends it. One we have not seen before is ArgumentError itself. clamp(low, high) needs low to be at most high:

dart
void main() {
  var mark = 105;
  print(mark.clamp(100, 0));
}
output
Unhandled exception:
Invalid argument(s): 100

Every one of these means the code is wrong. Swapping the two numbers fixes it for good.


6. The Exception Family: Expected Failures

ClassTypical cause
FormatExceptiontext that is not in the expected format — int.parse('12a'), double.parse('12,5')
IOException typesthe outside world fails: FileSystemException (missing file), SocketException (no network)
dart
void main() {
  var input = '12,5';
  print(double.parse(input));
}
output
Unhandled exception:
FormatException: Invalid double
12,5

Nothing is wrong with this code. A user typed a comma instead of a dot. Tomorrow another user will type something else. Files and I/O exceptions come in Category 18.


7. Fix Errors, Handle Exceptions

This is the rule of the whole category:

  • An Error → fix the code. The <= loop, the swapped clamp values, the missing base case. After the fix, the crash can never happen again.
  • An Exception → handle it. You cannot stop people from typing 'twenty'. Instead, plan what the program does when they do.

We already know one way to handle a bad number without crashing — tryParse returns null:

dart
void main() {
  var input = '12,5';
  var price = double.tryParse(input) ?? 0.0;
  print('Price: $price');
}
output
Price: 0.0

When no tryParse exists — reading a file, for example — Dart has try/catch, which looks like try { … } on FormatException { … }. That is the whole of Lecture 084. Catching an Error just to hide it is almost always wrong: the bug stays.


8. A Real-World Example

Sara's teacher types marks into a form. The text can be wrong (an Exception to plan for); the empty-list case is a bug to prevent in code:

dart
double average(List<int> marks) {
  var total = 0;
  for (var i = 0; i < marks.length; i++) {
    total += marks[i];
  }
  return total / marks.length;
}

void main() {
  var typed = ['85', 'abc', '92', '78'];
  var marks = <int>[];
  for (var text in typed) {
    var mark = int.tryParse(text);
    if (mark == null) {
      print('Skipped "$text" — not a number');
    } else {
      marks.add(mark);
    }
  }
  if (marks.isEmpty) {
    print('No valid marks');
  } else {
    print('Highest: ${marks.reduce((a, b) => a > b ? a : b)}');
    print('Average: ${average(marks)}');
  }
}
output
Skipped "abc" — not a number
Highest: 92
Average: 85.0

The loop uses <, so no RangeError. isEmpty guards reduce, so no StateError. And tryParse turns a would-be FormatException into a planned message.


Code Examples

Example 1 — A Runtime RangeError

dart
void main() {
  var names = ['Ali', 'Sara'];
  print(names[0]);
  print(names[2]);
}
output
Ali
Unhandled exception:
RangeError (length): Invalid value: Not in inclusive range 0..1: 2

Example 2 — A StateError

dart
void main() {
  var cart = <double>[];
  print(cart.first);
}
output
Unhandled exception:
Bad state: No element

Example 3 — A TypeError

dart
void main() {
  Object item = 20;
  print((item as String).length);
}
output
Unhandled exception:
type 'int' is not a subtype of type 'String' in type cast

Example 4 — A NoSuchMethodError

dart
void main() {
  dynamic score = 5;
  score.shout();
}
output
Unhandled exception:
NoSuchMethodError: Class 'int' has no instance method 'shout'.
Receiver: 5
Tried calling: shout()

With dynamic, Dart could not check the call before running (Lecture 011).

Example 5 — A FormatException

dart
void main() {
  print(int.parse('12a'));
}
output
Unhandled exception:
FormatException: Invalid radix-10 number (at character 1)
12a
^

Dart repeats the text it could not read, so you can see exactly what the user typed.

Example 6 — Checking the Family

dart
void main() {
  Object a = StateError('empty');
  Object b = ArgumentError('bad value');
  Object c = StackOverflowError();
  print(a is Error);
  print(b is Error);
  print(c is Error);
  print(b is Exception);
}
output
true
true
true
false

Example 7 — Student Marks: Fixing an Error

The crashing report from section 3, with <= changed to <:

dart
double average(List<int> marks) {
  var total = 0;
  for (var i = 0; i < marks.length; i++) {
    total += marks[i];
  }
  return total / marks.length;
}

void main() {
  print('Report for Ali');
  print('Average: ${average([85, 92, 78])}');
}
output
Report for Ali
Average: 85.0

How it works

  1. The trace pointed to main.dart:4, the marks[i] inside the loop.
  2. With <, i stops at 2, the last valid index.
  3. The fix is in the code; nothing needs to be "handled".

Example 8 — Shopping Total: Handling Bad Input

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

How it works

  1. int.parse('two') would throw a FormatException.
  2. tryParse returns null instead, and ?? chooses a default of 1.
  3. The input may be wrong next time too — so the program plans for it.

Example 9 — Bank Balance: Sorting Problems by Family

dart
String advice(Object problem) {
  return switch (problem) {
    Error() => 'Bug: fix the code',
    Exception() => 'Failure: handle it',
    _ => 'Unknown',
  };
}

void main() {
  print(advice(RangeError('account index 7')));
  print(advice(FormatException('amount "1o0"')));
  print(advice(StateError('no transactions')));
}
output
Bug: fix the code
Failure: handle it
Bug: fix the code

How it works

  1. Error() and Exception() are object patterns (Lecture 080) that test the type.
  2. RangeError and StateError extend Error; FormatException implements Exception.
  3. _ covers anything else, since Object has more subtypes than these two.

Example 10 — Temperature Readings

dart
void main() {
  var readings = ['21.5', 'ERR', '23.0', ''];
  var valid = <double>[];
  for (var text in readings) {
    var value = double.tryParse(text);
    if (value != null) {
      valid.add(value);
    }
  }
  print('Valid readings: $valid');
  if (valid.isNotEmpty) {
    print('Last: ${valid.last}');
  }
}
output
Valid readings: [21.5, 23.0]
Last: 23.0

How it works

  1. A sensor sending 'ERR' is an expected failure; tryParse skips it.
  2. valid.last on an empty list would be a StateError — a bug — so isNotEmpty prevents it.

Common Beginner Mistakes

1. Reading only the message, not the trace

RangeError … 0..2: 3 says what went wrong, not where. The first frame in your own file (main.dart:4:19) says where.

2. Stopping at frame #0

#0 is often inside dart:core. Dart's list code is not broken; skip down to your file.

3. Thinking "Unhandled exception" means an Exception

Dart prints those words for every crash. RangeError is still an Error.

4. Confusing a compile error with an Error object

Error: A value of type 'String' can't be assigned… comes from the compiler, before the program runs. The Error family is about objects thrown while it runs.

5. Using parse on input that may be wrong

dart
void main() {
  var typed = 'abc';
  // print(int.parse(typed)); // ✗ crashes for any non-number
  print(int.tryParse(typed) ?? 0);
}
output
0

6. Planning to "handle" a bug

A RangeError from <= is not bad luck — it happens every time. Fix the loop.


Best Practices

  • Read a stack trace from the first frame in your own file.
  • Treat every Error as a bug: change the code so it cannot happen.
  • Treat every Exception as a possible outcome: decide what the program does then.
  • Guard list operations with isEmpty / isNotEmpty and loops with <.
  • Prefer tryParse for text that comes from people or files.
  • Avoid dynamic and unchecked as casts — they move errors from compile time to runtime.

Mini Challenge

Zayan's program crashes. Read the error, find which family it belongs to, and fix it.

dart
void main() {
  var coins = [10, 5, 20];
  // print('Last coin: ${coins[coins.length]}'); // ✗
}
output
Unhandled exception:
RangeError (length): Invalid value: Not in inclusive range 0..2: 3

Try it yourself before opening the solution.

Show solution

RangeError is in the Error family — a bug — so we fix the code:

dart
void main() {
  var coins = [10, 5, 20];
  print('Last coin: ${coins.last}');
}
output
Last coin: 20

How it works

  1. The last valid index is length - 1, which is 2; coins[3] does not exist.
  2. last reads the final element without any index arithmetic.

Practice Questions

Beginner

  1. Sort into Error or Exception: RangeError, FormatException, StateError, TypeError, ArgumentError, FileSystemException.
  2. Which of these is a compile-time error, and which a runtime error? (a) int age = 'ten'; (b) print([1, 2][5]);
  3. Store Object p = StateError('x'); and print whether p is an Error and whether it is an Exception.

Application

  1. Read this trace. Which function, line, and column should you look at first?

    output
    #0      RangeError.checkValidRange (dart:core/errors.dart:392:9)
    #1      _StringBase.substring (dart:core-patch/string_patch.dart:437:27)
    #2      initials (file:///<your-folder>/main.dart:2:15)
    #3      greet (file:///<your-folder>/main.dart:6:19)
    #4      main (file:///<your-folder>/main.dart:11:3)
  2. Using the trace in question 4, list the order in which the functions were called, starting with main.

  3. This program crashes. Fix it so it prints No marks yet:

    dart
    void main() {
      var marks = <int>[];
      print(marks.first);
    }
  4. Read '7x' as a quantity without crashing; use 0 if it is not a number. Print Quantity: 0.

  5. Predict the full output, then name the class of the problem:

    dart
    void main() {
      print('A');
      print(10.clamp(20, 5));
      print('B');
    }

Challenge

  1. Write String family(Object problem) that returns 'Error', 'Exception', or 'Other'. Test it with ArgumentError('x'), FormatException('x'), and 'oops'.
  2. Sara's wallet app reads amounts typed by users: ['100', '2o0', '50']. Add the valid ones, report the bad one, and print the total. Then say which part of your program deals with an Exception and which part prevents an Error, if any.

Practice Answers

Question 1 — Solution

Error: RangeError, StateError, TypeError, ArgumentError. Exception: FormatException, FileSystemException.

Question 2 — Solution

(a) is a compile-time error — the program never starts. (b) compiles, then crashes at runtime with a RangeError.

Question 3 — Solution

dart
void main() {
  Object p = StateError('x');
  print(p is Error);
  print(p is Exception);
}
output
true
false

Question 4 — Solution

initials, line 2, column 15 — the first frame in your own file. Frames #0 and #1 are inside Dart's string code.

Question 5 — Solution

main → greet → initials → substring → checkValidRange. Read the trace bottom to top.

Question 6 — Solution

dart
void main() {
  var marks = <int>[];
  if (marks.isEmpty) {
    print('No marks yet');
  } else {
    print(marks.first);
  }
}
output
No marks yet

Question 7 — Solution

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

Question 8 — Solution

output
A
Unhandled exception:
Invalid argument(s): 20

B never prints. The class is ArgumentError — a bug, because 20 is larger than 5.

Question 9 — Solution

dart
String family(Object problem) {
  return switch (problem) {
    Error() => 'Error',
    Exception() => 'Exception',
    _ => 'Other',
  };
}

void main() {
  print(family(ArgumentError('x')));
  print(family(FormatException('x')));
  print(family('oops'));
}
output
Error
Exception
Other

Question 10 — Solution

dart
void main() {
  var typed = ['100', '2o0', '50'];
  var total = 0;
  for (var text in typed) {
    var amount = int.tryParse(text);
    if (amount == null) {
      print('Bad amount: $text');
    } else {
      total += amount;
    }
  }
  print('Total: $total');
}
output
Bad amount: 2o0
Total: 150

tryParse plus the null check handles the would-be FormatException. for-in never uses an index, so no RangeError is possible — the Error is prevented by design.


Quick Revision

  1. What is the difference between a compile-time error and a runtime error?
  2. What four things happen when a problem is not handled?
  3. What does one frame of a stack trace show?
  4. Which frame do you usually look at first?
  5. What kind of problem does the Error family represent? And Exception?
  6. Which family does Bad state: No element belong to?
  7. Why do we fix Errors but handle Exceptions?

Key Takeaways

  • Compile-time errors stop the program before it starts; runtime problems stop it midway.
  • An unhandled problem prints Unhandled exception:, a message, and a stack trace.
  • Each frame shows a function, a file, and line:column; start at the first frame in your own file.
  • Error = programmer bug: RangeError, TypeError, StateError, ArgumentError, NoSuchMethodError, StackOverflowError.
  • Exception = expected failure: FormatException, I/O exceptions.
  • Fix Errors in the code; handle Exceptions with a plan.
  • tryParse and guards like isEmpty already avoid many crashes.

What's Next?

Lecture 084 — try, catch & finally. tryParse handles bad numbers, but most operations that can fail have no "try" version. Next we learn try and catch, which let a program catch a thrown Exception, respond to it, and carry on — plus finally, which runs no matter what.


Video lecture

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