Exceptions and Error Handling
Programs fail. Files go missing, users type nonsense, networks drop. What separates a fragile program from a dependable one is not whether it ever hits problems — it is what happens when it does. In this chapter you will learn how Dart lets you detect problems, respond to them, and clean up after them, so a failure in one place does not silently corrupt the rest of your program.
This chapter assumes you already know how to write Dart functions and use classes. You do not need any prior experience with error handling.
What is an exception?
An exception is an object that represents a problem that happened while your program was running. When a problem occurs, Dart "throws" an exception, which means it creates that object and stops the current flow of code. The exception travels up until some code "catches" it and decides what to do. If nothing catches it, the program stops and prints the error.
Think of it like a fire alarm. Any point in the building can pull the alarm (throw). The alarm sound travels everywhere until someone responsible responds (catches). If nobody responds, the whole building shuts down.
Throwing an exception
You throw an exception with the throw keyword. Dart has a few built-in exception types, and you can also make your own.
void setAge(int age) {
if (age < 0) {
throw ArgumentError('Age cannot be negative, got $age');
}
print('Age set to $age');
}
void main() {
setAge(30); // works fine
setAge(-5); // throws and stops the program
print('never reached');
}ArgumentError is a built-in type that means "you passed me a bad argument." When the second call throws, the program prints the error and stops. The final print never runs.
Two built-in types appear constantly:
Exception: for expected problems, like a missing file or bad input.Error: for bugs, like using a variable that holds no value. Errors usually mean the code itself is wrong and should be fixed, not caught.
Catching exceptions
To respond to a problem instead of crashing, wrap the risky code in a try block. If an exception is thrown inside try, control jumps to the matching catch.
void main() {
try {
setAge(-5);
print('This line is skipped when setAge throws');
} catch (e) {
print('Caught a problem: $e');
}
print('The program continues normally');
}Run this and you will see the caught message, then the final line. The program recovers instead of stopping.
You can catch specific types by writing on Type. This matters because different problems deserve different responses. A missing file might mean "ask the user for another path," while bad input might mean "show a validation message."
class OutOfStock implements Exception {
final String item;
OutOfStock(this.item);
@override
String toString() => 'Out of stock: $item';
}
void order(String item, int stock) {
if (stock == 0) throw OutOfStock(item);
print('Ordered $item');
}
void main() {
try {
order('coffee', 0);
} on OutOfStock catch (e) {
print('Sorry, $e. Try another item.');
} catch (e) {
print('Unexpected problem: $e');
}
}The on OutOfStock catch (e) branch handles our custom exception. The plain catch (e) is the safety net for everything else. You can also write on OutOfStock with no variable if you do not need the details.
There is a third clause, finally, which runs no matter what happens — success, exception, or rethrow. This is where cleanup belongs, like closing a file or a database connection.
void main() {
try {
print('opening resource');
throw StateError('something broke');
} catch (e) {
print('caught: $e');
} finally {
print('closing resource — always runs');
}
}Catching and rethrowing
Sometimes you want to record a problem but not fully swallow it. Use rethrow to pass the exception up to whoever called you, after doing your own handling first.
void saveData() {
try {
throw Exception('disk full');
} catch (e) {
print('logging the failure: $e');
rethrow; // let the caller decide what to do next
}
}
void main() {
try {
saveData();
} catch (e) {
print('the app shows an error dialog: $e');
}
}A common mistake is writing an empty catch block that hides the problem. If you catch an exception and do nothing, your program may keep running with bad data, and the real bug becomes much harder to find later. If you are not going to handle an exception meaningfully, do not catch it at all — or rethrow it.
Making your own exceptions
Custom exception types are just small classes. Making a class implement Exception (or Error for bugs) gives your failures a name your code can catch precisely. The OutOfStock example above is the standard pattern: a class with a field and a helpful toString. Use @override String toString() so the printed message is readable when the exception reaches a log or a crash report.
Try, but do not fear
Dart's analyzer is strict about types, so many potential problems — a variable that might hold nothing, a value of the wrong kind — are caught before your program even runs. Exceptions are for the problems that remain: the outside world. Network calls, file systems, user input, and hardware all misbehave eventually, and try/catch is how you stay in control when they do.
A good habit: throw exceptions from low-level code when something is impossible to continue from, and catch them higher up where you know what the user was trying to do and can respond sensibly.
How this connects to async code
One more thing before you move on: exceptions and asynchronous code need each other. In the next chapter you will meet async and await, and you will see that a plain try/catch block also catches exceptions thrown by awaited asynchronous calls — one of the reasons Dart's approach to async is so pleasant to work with. Everything you learned here carries over directly.
Exercise
Exercise
A function reads a file, and you want to make sure the file is closed whether or not reading succeeds. Where should the closing code go, and why?
Show answer
In the finally clause of a try block. Code in finally runs whether the try block succeeds or throws, so the file is always closed. Putting it only after the try block would be skipped whenever an exception occurs.
Exercise
What is the difference between catch (e) and on OutOfStock catch (e)?
Show answer
on OutOfStock catch (e) catches only exceptions of type OutOfStock, letting other exceptions pass through to other handlers. A bare catch (e) catches every exception. Catching specific types lets you give each kind of problem its own response.
Spotted a mistake, or code that does not work?