Exception Handling in Dart

By Syncfusion®, Inc. · Last updated

How to catch errors with try, on, catch and finally, throw and rethrow them, write your own exceptions, and read Dart's built-in errors.

What Is an Exception?

Exception = a problem that happens while the program runs, and stops the normal flow.

  • A user types abc where a number was expected.
  • A file the program needs is missing.
  • A list is empty, but the code asks for its first item.
  • If nobody handles the problem, the program crashes and prints Unhandled exception.

Simple lesson: an exception is a "something went wrong" signal, and you can catch it so the program keeps going.

A Real-World Example

Think of a school bus trip.

  • The plan: go to the museum, see the exhibits, come back.
  • Problem: a tyre goes flat on the way. That is the exception.
  • The driver does not give up the whole trip. He has a plan: change the tyre, or call another bus. That is handling.
  • And whatever happens, everyone does a head count at the end. That is finally.

Choose what a student typed, and watch which blocks run:

Every Run box on this page runs on the course's Dart runner in your browser.

Some boxes fail on purpose, to show the error Dart prints. They are marked as expected errors.

What You Will Learn

In this lesson, you will learn:

  1. The kinds of mistakes in a program, and how errors differ from exceptions.
  2. How try, on, catch and finally work, and in what order handlers are tried.
  3. How to read a stack trace.
  4. How to throw, rethrow, and make your own exceptions.
  5. The common built-in errors and exceptions.

Kinds of Problems

Syntax, Runtime and Logic Errors

KindWhen it is foundExampleWho catches it
Syntax / compile errorbefore runninga missing ;Dart, before the program starts
Runtime errorwhile runningint.parse('abc')your try and catch
Logic errornever, by Darttotal = a - b when you meant +only testing

try and catch handle runtime problems. They cannot fix wrong logic or wrong syntax.

An unhandled exception crashes the program

void main() {
  print('Reading marks...');
  var marks = int.parse('ninety');
  print('Marks: $marks');
}

The last line never runs. The program stopped at int.parse.

Errors vs Exceptions

ExceptionError
Meansan expected problem from outsidea bug in the code
Examplebad user input, missing fileindex out of range, null where not allowed
Should you catch it?yes, and recoverusually no: fix the code
Built-in examplesFormatException, TimeoutExceptionRangeError, StateError, TypeError

Simple lesson: catch exceptions to recover, and fix the code that causes errors.

try, on, catch and finally

The Basic Shape

try {
  // code that might fail
} on SomeException {
  // runs for that type
} catch (e) {
  // runs for anything else, e is the problem
} finally {
  // runs every time
}

Reading marks safely

void main() {
  for (var text in ['85', 'abc']) {
    try {
      var marks = int.parse(text);
      print('Marks: $marks');
    } on FormatException {
      print('"$text" is not a number');
    } finally {
      print('Checked "$text"');
    }
  }
  print('The program is still running.');
}

on, catch and Both

  • on Type: catch only this type. You do not get the object.
  • catch (e): catch anything. e is the thrown object.
  • on Type catch (e): catch only this type, and get the object.

on Type catch (e) gives you the details

void main() {
  try {
    int.parse('12a');
  } on FormatException catch (e) {
    print('Problem: ${e.message}');
    print('Type: ${e.runtimeType}');
  }
}

Matching Order

Handlers are tried from top to bottom. The first one that fits runs. The rest are skipped.

Which handler catches it?

Put specific types first and a general catch (e) last.

If catch (e) comes first, it catches everything, and the specific handlers below it never run.

finally

finally = a block that runs every time: after success, after a caught error, and even when the error is not caught.

  • Use it for clean-up: close a file, stop a timer, print "done".

Simple lesson: try the risky code, on/catch the problems you expect, and clean up in finally.

Stack Traces

Stack trace = the list of function calls that were running when the problem happened. It shows where it went wrong.

  • Get it as the second name in catch: catch (e, s).
  • Read it from the top: the top line is where the error was thrown.
void main() {
  try {
    loadMarks();
  } catch (e, s) {
    print(e);
    print(s);
  }
}
// FormatException: Invalid radix-10 number (at character 1)
// #0      int.parse (dart:core-patch/integers_patch.dart:52:7)
// #1      loadMarks (file:///school/bin/main.dart:12:3)
// #2      main (file:///school/bin/main.dart:3:5)
  • Line #1 says the problem came from loadMarks, line 12.
  • The course runner prints a short stack trace. Your computer prints the full one.

Simple lesson: a stack trace is a trail of footprints that leads you to the line that failed.

throw and rethrow

Throwing Your Own Problem

throw = signal a problem yourself. Usually you throw an Exception or an Error object.

Throwing when the input is wrong

int checkMarks(int marks) {
  if (marks < 0 || marks > 100) {
    throw ArgumentError('Marks must be 0 to 100, not $marks');
  }
  return marks;
}

void main() {
  try {
    print(checkMarks(88));
    print(checkMarks(140));
  } on ArgumentError catch (e) {
    print(e.message);
  }
}

Propagation

Propagation = an uncaught exception rises up out of each function, to the one that called it, until a catch stops it.

The problem rises from parse to main

int parse(String text) => int.parse(text);

int loadMarks(String text) {
  print('loadMarks starts');
  var m = parse(text);
  print('loadMarks ends');
  return m;
}

void main() {
  try {
    loadMarks('x9');
  } on FormatException {
    print('main caught it');
  }
}

loadMarks ends never prints. The exception jumped straight out of loadMarks.

rethrow

rethrow = inside a catch, pass the same problem on to the caller. Useful when you want to note it, but not handle it here.

Note it, then pass it on

void saveMarks(String text) {
  try {
    int.parse(text);
  } catch (e) {
    print('saveMarks: could not save "$text"');
    rethrow;
  }
}

void main() {
  try {
    saveMarks('ten');
  } on FormatException catch (e) {
    print('main: ${e.message}');
  }
}

Simple lesson: throw starts a problem, it rises until caught, and rethrow sends a caught one further up.

Custom Exceptions

Custom exception = your own exception class. Make it implements Exception.

A fee exception with details

class FeeNotPaidException implements Exception {
  final String student;
  final int due;
  FeeNotPaidException(this.student, this.due);

  @override
  String toString() => 'FeeNotPaidException: $student owes Rs. $due';
}

void issueHallTicket(String name, int due) {
  if (due > 0) {
    throw FeeNotPaidException(name, due);
  }
  print('Hall ticket issued to $name');
}

void main() {
  for (var (name, due) in [('Asha', 0), ('Ravi', 1500)]) {
    try {
      issueHallTicket(name, due);
    } on FeeNotPaidException catch (e) {
      print(e);
      print('Please pay Rs. ${e.due} at the office.');
    }
  }
}
  • A custom type lets callers catch exactly this problem with on FeeNotPaidException.
  • Fields like due carry the details.
  • Override toString() so the message is clear.

Nested Handlers

A try can sit inside another. The inner one gets the first chance:

Inner handler first, outer handler for the rest

void main() {
  try {
    try {
      var data = [1, 2];
      print(data[9]);
    } on FormatException {
      print('inner: format problem');
    } finally {
      print('inner finally');
    }
  } on RangeError {
    print('outer: position problem');
  }
}

The inner try had no handler for RangeError, so it rose to the outer one. The inner finally still ran first.

Simple lesson: make your own exception classes so callers can catch exactly the problem they understand.

Built-in Errors and Exceptions

NameThrown whenExample
FormatExceptiontext is in the wrong formatint.parse('abc')
ArgumentErrora function gets a bad argumentyour own throw ArgumentError(...)
RangeErroran index or value is out of range[1, 2][5]
StateErrorthe object is in the wrong state[].first
UnsupportedErrorthe operation is not allowedadding to a const list
UnimplementedErrora method is not written yetthrow UnimplementedError();
TypeErrora value has the wrong type, or ! on nullString? s; s!.length
NoSuchMethodErrora missing method is called on a dynamic valuedynamic d = 5; d.fly();
AssertionErroran assert fails (when asserts are on)assert(age > 0)
ConcurrentModificationErrora list changes while a loop reads itfor (x in l) l.add(x)

Meeting a few built-in errors

void main() {
  try {
    var empty = <int>[];
    print(empty.first);
  } on StateError catch (e) {
    print('StateError: ${e.message}');
  }

  try {
    const houses = ['Red', 'Blue'];
    houses.add('Pink');
  } on UnsupportedError catch (e) {
    print('UnsupportedError: ${e.message}');
  }

  try {
    var marks = [80, 90];
    for (var m in marks) {
      marks.add(m + 1);
    }
  } on ConcurrentModificationError {
    print('ConcurrentModificationError: do not change a list while looping over it');
  }
}

Easy way to remember

  • try: "let me try this".
  • on Type: "if it is this problem...".
  • catch (e, s): "...give me the problem and where it happened".
  • finally: "no matter what, do this".
  • throw / rethrow: "raise a problem / pass it on".

Simple lesson: the built-in names tell you what went wrong, so read the first word of every error.

TL;DR

  1. An exception is a runtime problem. Unhandled, it crashes the program.
  2. try risky code. on Type, catch (e, s) handle it. finally always runs.
  3. Handlers are tried top to bottom. Specific first, general catch last.
  4. throw raises a problem. It propagates up until caught. rethrow passes it on.
  5. Custom exceptions: class X implements Exception. Catch Exceptions, fix Errors.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
catch (e) before on FormatExceptionOrder does not matterThe first match winsThe specific handler never runs.
An empty catch (e) {}Hiding errors is safeAt least print or log itBugs disappear silently.
Catching Errors to hide bugsAll problems should be caughtFix the code that causes errorsThe bug stays in the program.
throw e; inside catchSame as rethrowrethrow keeps the original stack traceThe trail to the real line is lost.
A custom exception that extends nothingAny class can be thrownimplements Exception makes intent clearon Exception will not catch it.
Expecting code after a throw to runDart finishes the blockControl jumps to the handlerOutput you expected never appears.

Check Yourself

Exercises

What to Remember

  • An exception is a runtime problem. Unhandled, it stops the program.
  • try the risky code, handle it with on and catch, and clean up in finally.
  • Handlers are tried top to bottom, so put specific types first.
  • throw raises a problem, it rises until caught, and rethrow passes it on.
  • Make custom exceptions with implements Exception, and fix the code behind Errors.

Next: Async Basics and Futures. You will run slow work without freezing your program.