On this page21 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. Throwing an Exception
- 2. Three Built-in Types to Throw
- 3. Catching What You Throw
- 4. throw Is an Expression
- 5. Functions That Return Never
- 6. A Custom Exception Class
- 7. A Hierarchy of Exceptions
- 8. Protecting Invariants in Constructors and Setters
- 9. 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 85 — throw & Custom Exceptions
| Category | 14 — Error & Exception Handling |
| Lecture | 085 of 140 |
| Estimated video length | 11 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
Until now, a method given a bad value could only print a message or return
false. Withthrow, your code raises an exception the caller cannot ignore, and custom exception classes say exactly what went wrong.
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. Stack trace lines afterUnhandled exception:are left out to save space.
Prerequisites
Students should already understand, from Lectures 030–084:
- functions,
=>, and??(Lectures 014, 030–035) - classes, constructors, setters, private members, inheritance,
implements(053–063) - the
Nevertype (075) and switch expressions (081) - errors vs exceptions (083), and
try/on Type catch (e)(084)
Connection With Previous Lectures
In Lecture 057, BankAccount.withdraw returned false for a bad amount. The caller can
simply ignore that false, and a false cannot say why: a negative amount, or not enough
money? In Lecture 084 you learned to catch the exceptions Dart throws. Now our own code
throws them, with our own types that carry the details.
Learning Objectives
By the end of this lecture you will be able to:
- Throw built-in errors and exceptions with
throw - Choose between
ArgumentError,StateError, andFormatException - Use
throwas an expression after??and=> - Write a function whose return type is
Never - Create a custom exception class with fields and a
toString - Build a small hierarchy of exceptions and catch them by type
- Protect an object's invariants by throwing from constructors and setters
1. Throwing an Exception
throw stops the current function immediately and hands an object up to whoever called
it. If nobody catches it, the program ends.
Analogy — a referee's whistle stops play at once, and the card shows what the foul was.
throwis the whistle; the object you throw is the card.
void deposit(int amount) {
if (amount <= 0) {
throw ArgumentError('Deposit must be positive: $amount');
}
print('Deposited $amount');
}
void main() {
deposit(500);
deposit(-50);
print('Done');
}Deposited 500
Unhandled exception:
Invalid argument(s): Deposit must be positive: -50'Done' never prints: nothing after the throw runs.
2. Three Built-in Types to Throw
Dart already has a class for the most common problems:
| Throw this | When | It prints as |
|---|---|---|
ArgumentError | a caller passed a bad value | Invalid argument(s): … |
StateError | the object is not ready for this call | Bad state: … |
FormatException | text is not in the expected format | FormatException: … |
You met two already: int.parse('twenty') (Lecture 010) and first on an empty list
(Lecture 040). Example 2 below throws a StateError from a class whose call came at the wrong
moment.
3. Catching What You Throw
Your own throws are caught like Dart's, with on Type catch (e) (Lecture 084).
ArgumentError.value records the bad value and parameter name too:
int parseMarks(String text) {
var marks = int.tryParse(text);
if (marks == null) {
throw FormatException('Marks must be a number', text);
}
if (marks < 0 || marks > 100) {
throw ArgumentError.value(marks, 'marks', 'must be 0 to 100');
}
return marks;
}
void main() {
for (var text in ['85', 'abc', '120']) {
try {
print('Saved ${parseMarks(text)}');
} on FormatException catch (e) {
print('Not a number: ${e.source}');
} on ArgumentError catch (e) {
print('${e.name} ${e.message}, got ${e.invalidValue}');
}
}
}Saved 85
Not a number: abc
marks must be 0 to 100, got 120Each failure lands in its own on block.
4. throw Is an Expression
throw can sit where a value is expected. After ?? it means "use this value, or fail"; put
it in parentheses there:
String findPhone(Map<String, String> phones, String name) =>
phones[name] ?? (throw ArgumentError('No phone for $name'));
void main() {
var phones = {'Ali': '0300-111', 'Sara': '0321-222'};
print(findPhone(phones, 'Sara'));
print(findPhone(phones, 'Zayan'));
}0321-222
Unhandled exception:
Invalid argument(s): No phone for ZayanThe result type is String, not String?: either we return a phone number or we never return
at all. throw also works as an => body, for example as the _ => arm of a switch
expression that rejects every value the other arms did not handle (see Example 5).
5. Functions That Return Never
In Lecture 075 you met Never, the type with no values. A function that always throws has
nothing to return, so its return type is Never:
Never fail(String message) {
throw StateError(message);
}
String describe(int? stock) {
if (stock == null) fail('Stock unknown');
return 'In stock: ${stock + 1}';
}
void main() {
print(describe(3));
print(describe(null));
}In stock: 4
Unhandled exception:
Bad state: Stock unknownstock + 1 needs no !: because fail never returns, Dart knows stock is not null after
the if, and promotes it to int.
6. A Custom Exception Class
Built-in types say what kind of problem happened, not the details. A custom exception is an
ordinary class that implements Exception:
class InsufficientFundsException implements Exception {
final int balance;
final int requested;
InsufficientFundsException(this.balance, this.requested);
int get shortfall => requested - balance;
@override
String toString() =>
'InsufficientFundsException: wanted $requested, only $balance available';
}
void withdraw(int balance, int amount) {
if (amount > balance) {
throw InsufficientFundsException(balance, amount);
}
print('Paid $amount');
}
void main() {
try {
withdraw(300, 500);
} on InsufficientFundsException catch (e) {
print(e);
print('Sorry, you need ${e.shortfall} more.');
}
}InsufficientFundsException: wanted 500, only 300 available
Sorry, you need 200 more.implements Exception marks the class as an exception. The fields carry the facts, and
toString builds the message (without it you would see Instance of 'InsufficientFundsException'). Because e has the custom type, the caller can read
e.shortfall, which a bool result could never provide.
7. A Hierarchy of Exceptions
Related exceptions can share a parent, so a caller can catch one problem or the whole family:
abstract class BankException implements Exception {
final String message;
BankException(this.message);
@override
String toString() => message;
}
class InvalidAmountException extends BankException {
InvalidAmountException(int amount) : super('Invalid amount: $amount');
}
class InsufficientFundsException extends BankException {
InsufficientFundsException(int balance, int requested)
: super('Wanted $requested, only $balance available');
}
void withdraw(int balance, int amount) {
if (amount <= 0) throw InvalidAmountException(amount);
if (amount > balance) throw InsufficientFundsException(balance, amount);
print('Paid $amount');
}
void main() {
for (var amount in [200, -5, 900]) {
try {
withdraw(500, amount);
} on InsufficientFundsException catch (e) {
print('Low balance. $e');
} on BankException catch (e) {
print('Bank problem. $e');
}
}
}Paid 200
Bank problem. Invalid amount: -5
Low balance. Wanted 900, only 500 availableon clauses are checked top to bottom, so put the specific type first and the parent
last; -5 falls through to on BankException.
8. Protecting Invariants in Constructors and Setters
Lecture 057's invariant was the balance is never negative. Its constructor quietly turned
-300 into 0, so Sara got an account she never asked for. Throwing refuses to build a bad
object at all:
class InsufficientFundsException implements Exception {
final int balance;
final int requested;
InsufficientFundsException(this.balance, this.requested);
}
class BankAccount {
final String owner;
int _balance;
BankAccount(this.owner, this._balance) {
if (_balance < 0) {
throw ArgumentError.value(_balance, 'balance', 'cannot be negative');
}
}
int get balance => _balance;
void withdraw(int amount) {
if (amount > _balance) throw InsufficientFundsException(_balance, amount);
_balance -= amount;
}
}
void main() {
var ali = BankAccount('Ali', 1000);
ali.withdraw(400);
print('Ali: ${ali.balance}');
var sara = BankAccount('Sara', -300);
print('Sara: ${sara.balance}');
}Ali: 600
Unhandled exception:
Invalid argument (balance): cannot be negative: -300sara is never created; a negative balance cannot exist. withdraw now
returns void: success is the normal path, and failure cannot be ignored.
A setter guards an invariant the same way. Lecture 056's marks setter printed Rejected; now
it throws a RangeError (an ArgumentError for numbers out of range) before storing anything:
set marks(int value) {
if (value < 0 || value > 100) throw RangeError.range(value, 0, 100, 'marks');
_marks = value;
}Setting -40 throws RangeError (marks): Invalid value: Not in inclusive range 0..100: -40.
9. A Real-World Example
A small bank processing a list of transfers:
abstract class BankException implements Exception {
final String message;
BankException(this.message);
@override
String toString() => message;
}
class AccountNotFoundException extends BankException {
AccountNotFoundException(String name) : super('No account for $name');
}
class InsufficientFundsException extends BankException {
InsufficientFundsException(String owner, int shortfall)
: super('$owner is short by $shortfall');
}
class Bank {
final Map<String, int> _balances = {'Ali': 1000, 'Sara': 200};
int _find(String name) =>
_balances[name] ?? (throw AccountNotFoundException(name));
void transfer(String from, String to, int amount) {
if (amount <= 0) throw ArgumentError.value(amount, 'amount');
var fromBalance = _find(from);
var toBalance = _find(to);
if (amount > fromBalance) {
throw InsufficientFundsException(from, amount - fromBalance);
}
_balances[from] = fromBalance - amount;
_balances[to] = toBalance + amount;
print('OK: $from -> $to $amount');
}
void report() => print(_balances);
}
void main() {
var bank = Bank();
var transfers = [
('Ali', 'Sara', 300),
('Sara', 'Zayan', 50),
('Sara', 'Ali', 900),
];
for (var (from, to, amount) in transfers) {
try {
bank.transfer(from, to, amount);
} on InsufficientFundsException catch (e) {
print('Declined: $e');
} on BankException catch (e) {
print('Failed: $e');
}
}
bank.report();
}OK: Ali -> Sara 300
Failed: No account for Zayan
Declined: Sara is short by 400
{Ali: 700, Sara: 500}Every check throws before a balance changes, so failed transfers change nothing. Nobody
catches the ArgumentError on purpose: as Lecture 083 explained, an Error means the calling
code has a bug to fix, while an Exception is a problem a correct program handles.
Code Examples
Example 1 — Rejecting a Bad Argument
double toFahrenheit(double celsius) {
if (celsius < -273.15) {
throw ArgumentError('Below absolute zero: $celsius');
}
return celsius * 9 / 5 + 32;
}
void main() {
print(toFahrenheit(25));
try {
toFahrenheit(-300);
} on ArgumentError catch (e) {
print(e);
}
}77.0
Invalid argument(s): Below absolute zero: -300.0Example 2 — A StateError for the Wrong Moment
class Order {
bool _paid = false;
void pay() => _paid = true;
void ship() {
if (!_paid) throw StateError('Order not paid yet');
print('Shipped');
}
}
void main() {
var order = Order();
try {
order.ship();
} on StateError catch (e) {
print(e);
}
order.pay();
order.ship();
}Bad state: Order not paid yet
ShippedExample 3 — A FormatException for Bad Text
(String, int) parseEntry(String line) {
var parts = line.split(':');
if (parts.length != 2) {
throw FormatException('Expected name:marks', line);
}
return (parts[0], int.parse(parts[1]));
}
void main() {
print(parseEntry('Ali:80'));
try {
parseEntry('Sara-90');
} on FormatException catch (e) {
print('${e.message} in "${e.source}"');
}
}(Ali, 80)
Expected name:marks in "Sara-90"Example 4 — ?? (throw ...)
void main() {
var prices = {'pen': 50, 'book': 450};
for (var item in ['book', 'bag']) {
try {
var price = prices[item] ?? (throw StateError('No price for $item'));
print('$item: $price');
} on StateError catch (e) {
print(e);
}
}
}book: 450
Bad state: No price for bagExample 5 — throw in a Switch Arm
String dayName(int day) => switch (day) {
1 => 'Monday',
2 => 'Tuesday',
_ => throw RangeError.range(day, 1, 2, 'day'),
};
void main() {
print(dayName(2));
try {
print(dayName(9));
} on RangeError catch (e) {
print(e);
}
}Tuesday
RangeError (day): Invalid value: Not in inclusive range 1..2: 9Example 6 — A Never Helper
Never missing(String field) {
throw FormatException('Missing field: $field');
}
void main() {
var form = {'name': 'Zayan'};
var name = form['name'] ?? missing('name');
print('Hello, $name');
var city = form['city'] ?? missing('city');
print(city);
}Hello, Zayan
Unhandled exception:
FormatException: Missing field: cityExample 7 — Student Marks
class InvalidMarksException implements Exception {
final int marks;
InvalidMarksException(this.marks);
}
void main() {
var sheet = {'Ali': 80, 'Sara': 105, 'Zayan': 70};
sheet.forEach((student, marks) {
try {
if (marks > 100) throw InvalidMarksException(marks);
print('$student: $marks');
} on InvalidMarksException catch (e) {
print('Check $student: ${e.marks} is above 100');
}
});
}Ali: 80
Check Sara: 105 is above 100
Zayan: 70Example 8 — Shopping Total
class OutOfStockException implements Exception {
final String item;
final int available;
OutOfStockException(this.item, this.available);
}
void main() {
var stock = {'notebook': 10, 'pen': 2};
var total = 0;
try {
for (var (item, qty, price) in [('notebook', 3, 500), ('pen', 5, 50)]) {
if (qty > stock[item]!) throw OutOfStockException(item, stock[item]!);
total += qty * price;
}
} on OutOfStockException catch (e) {
print('Only ${e.available} ${e.item}(s) left');
}
print('Total: $total');
}Only 2 pen(s) left
Total: 1500Example 9 — Bank Balance
class BankException implements Exception {
final String message;
BankException(this.message);
@override
String toString() => message;
}
class FrozenException extends BankException {
FrozenException(String owner) : super('$owner is frozen');
}
void main() {
var all = [('Ali', false, 500), ('Sara', false, 50), ('Zayan', true, 900)];
for (var (owner, frozen, balance) in all) {
try {
if (frozen) throw FrozenException(owner);
if (balance < 100) throw BankException('$owner is short');
print('$owner paid 100');
} on BankException catch (e) {
print('Failed: $e');
}
}
}Ali paid 100
Failed: Sara is short
Failed: Zayan is frozenOne on BankException catches the parent and every child.
Example 10 — Temperature Readings
class SensorException implements Exception {
final String reading;
SensorException(this.reading);
}
double parseReading(String text) =>
double.tryParse(text) ?? (throw SensorException(text));
void main() {
var valid = <double>[];
for (var text in ['21.5', 'ERR', '23.5']) {
try {
valid.add(parseReading(text));
} on SensorException catch (e) {
print('Skipped "${e.reading}"');
}
}
print('Average: ${(valid[0] + valid[1]) / valid.length}');
}Skipped "ERR"
Average: 22.5Common Beginner Mistakes
1. throw after ?? without parentheses
int parse(String? text) {
// return text?.length ?? throw ArgumentError('No text'); // ✗
return text?.length ?? (throw ArgumentError('No text')); // ✓
}Error: 'throw' can't be used as an identifier because it's a keyword.2. Extending Exception
// class PaymentException extends Exception {} // ✗
class PaymentException implements Exception {}
void main() {}Error: The class 'Exception' can't be extended outside of its library because it's an interface class.Use implements Exception; extending your own base class, like BankException, is fine.
3. Forgetting the throw keyword
void deposit(int amount) {
if (amount <= 0) {
ArgumentError('Bad amount'); // ✗ creates the object, throws nothing
}
print('Deposited $amount');
}
void main() => deposit(-50);Deposited -50No error, which is exactly the problem.
4. Catching the parent before the child
class BankException implements Exception {}
class InsufficientFundsException extends BankException {}
void main() {
try {
throw InsufficientFundsException();
} on BankException {
print('Bank problem');
} on InsufficientFundsException {
print('Low balance'); // never reached
}
}Bank problemdart analyze flags the dead block:
warning - Dead code: This on-catch block won't be executed because 'InsufficientFundsException' is a subtype of 'BankException' and hence will have been caught already.Best Practices
- Throw as soon as you detect a problem, before changing any state.
- Throw
Errors for mistakes in calling code; throwExceptions for problems a correct program should handle. - Name custom exceptions with an
Exceptionsuffix and useimplements Exception. - Give custom exceptions
finalfields for the details and a cleartoString. - Throw objects, never strings:
throw 'No money'runs, but cannot be caught by a useful type.
Mini Challenge
Build a Thermostat with int _target = 20, a getter, and a setter that throws a custom
TemperatureOutOfRangeException (field requested) outside 16..28. Set 22, then 35,
catching the failure, and print the final target.
Your program should print exactly:
Target set to 22
Rejected: 35
Final target: 22Try it yourself before opening the solution.
Show solution
class TemperatureOutOfRangeException implements Exception {
final int requested;
TemperatureOutOfRangeException(this.requested);
}
class Thermostat {
int _target = 20;
int get target => _target;
set target(int value) {
if (value < 16 || value > 28) throw TemperatureOutOfRangeException(value);
_target = value;
}
}
void main() {
var thermostat = Thermostat();
for (var value in [22, 35]) {
try {
thermostat.target = value;
print('Target set to $value');
} on TemperatureOutOfRangeException catch (e) {
print('Rejected: ${e.requested}');
}
}
print('Final target: ${thermostat.target}');
}Target set to 22
Rejected: 35
Final target: 22The setter throws before _target changes, so the print after it is skipped.
Practice Questions
Beginner
- Write
int square(int n)that throwsArgumentError('n must not be negative')for a negativen. Callsquare(4)andsquare(-2)in onetry, and print the caught error. - What do
print(FormatException('Bad date'))andprint(StateError('Closed'))show? - Write
class LampwhoseturnOff()throwsStateError('Lamp is already off')whilebool _onisfalse. Call it on a new lamp, catch, and print.
Application
- Write an arrow function
String capital(String country)that looks up{'Japan': 'Tokyo'}and throwsStateError('Unknown: $country')with?? (throw ...). Printcapital('Japan'), then catch and printcapital('Peru'). - Write
Never invalid(String reason)that throwsArgumentError(reason), and use it inint age(int? years)to rejectnullwith'Age is required'. Printage(21), then catch and printage(null). - Create
EmptyCartException implements ExceptionwhosetoStringreturns'Cart is empty'. Writeint checkout(List<int> prices)that throws it for an empty list and returns the sum otherwise. Printcheckout([500, 250]), then catch and printcheckout([]). - Create
PasswordTooShortExceptionwith a fieldlength. Throw it fromsetPassword('abc')(minimum 8 characters) and printToo short: 3.
Challenge
- Create
LibraryException(fieldmessage,toString) and a childBookNotFoundException(title)sayingNot found: <title>.borrow(title)reads{'Dart Basics': true}(true= borrowed), throws the child for a missing title andLibraryException('Already borrowed: <title>')for a borrowed one. Borrow'Dart Basics'and'Python 101'with oneon LibraryException. - Write
BankAccount(this._balance)that throwsArgumentError.valuewhen negative, andwithdrawthat throwsInsufficientFundsException(fieldsbalance,requested). Ali opens with1000, withdraws400, then700. PrintAli needs 100 more. - Zayan's
ScoreCardhasint _score = 0and a setter that throwsRangeError.range(value, 0, 50, 'score'). Set30,60, then45, printing each caught error, and finish withScore: 45.
Practice Answers
Question 1 — Solution
int square(int n) {
if (n < 0) throw ArgumentError('n must not be negative');
return n * n;
}
void main() {
try {
print(square(4));
print(square(-2));
} on ArgumentError catch (e) {
print(e);
}
}16
Invalid argument(s): n must not be negativeQuestion 2 — Solution
void main() {
print(FormatException('Bad date'));
print(StateError('Closed'));
}FormatException: Bad date
Bad state: ClosedQuestion 3 — Solution
class Lamp {
bool _on = false;
void turnOff() {
if (!_on) throw StateError('Lamp is already off');
_on = false;
}
}
void main() {
try {
Lamp().turnOff();
} on StateError catch (e) {
print(e);
}
}Bad state: Lamp is already offQuestion 4 — Solution
String capital(String country) =>
{'Japan': 'Tokyo'}[country] ?? (throw StateError('Unknown: $country'));
void main() {
print(capital('Japan'));
try {
capital('Peru');
} on StateError catch (e) {
print(e);
}
}Tokyo
Bad state: Unknown: PeruQuestion 5 — Solution
Never invalid(String reason) => throw ArgumentError(reason);
int age(int? years) {
if (years == null) invalid('Age is required');
return years;
}
void main() {
print(age(21));
try {
age(null);
} on ArgumentError catch (e) {
print(e);
}
}21
Invalid argument(s): Age is requiredreturn years; needs no ! because invalid returns Never.
Question 6 — Solution
class EmptyCartException implements Exception {
@override
String toString() => 'Cart is empty';
}
int checkout(List<int> prices) {
if (prices.isEmpty) throw EmptyCartException();
return prices.reduce((a, b) => a + b);
}
void main() {
print(checkout([500, 250]));
try {
checkout([]);
} on EmptyCartException catch (e) {
print(e);
}
}750
Cart is emptyQuestion 7 — Solution
class PasswordTooShortException implements Exception {
final int length;
PasswordTooShortException(this.length);
}
void setPassword(String p) {
if (p.length < 8) throw PasswordTooShortException(p.length);
}
void main() {
try {
setPassword('abc');
} on PasswordTooShortException catch (e) {
print('Too short: ${e.length}');
}
}Too short: 3Question 8 — Solution
class LibraryException implements Exception {
final String message;
LibraryException(this.message);
@override
String toString() => message;
}
class BookNotFoundException extends LibraryException {
BookNotFoundException(String title) : super('Not found: $title');
}
final books = {'Dart Basics': true};
void borrow(String title) {
var borrowed = books[title] ?? (throw BookNotFoundException(title));
if (borrowed) throw LibraryException('Already borrowed: $title');
}
void main() {
for (var title in ['Dart Basics', 'Python 101']) {
try {
borrow(title);
} on LibraryException catch (e) {
print(e);
}
}
}Already borrowed: Dart Basics
Not found: Python 101Question 9 — Solution
class InsufficientFundsException implements Exception {
final int balance;
final int requested;
InsufficientFundsException(this.balance, this.requested);
}
class BankAccount {
int _balance;
BankAccount(this._balance) {
if (_balance < 0) throw ArgumentError.value(_balance, 'balance');
}
void withdraw(int amount) {
if (amount > _balance) throw InsufficientFundsException(_balance, amount);
_balance -= amount;
}
}
void main() {
var ali = BankAccount(1000);
ali.withdraw(400);
try {
ali.withdraw(700);
} on InsufficientFundsException catch (e) {
print('Ali needs ${e.requested - e.balance} more');
}
}Ali needs 100 moreQuestion 10 — Solution
class ScoreCard {
int _score = 0;
int get score => _score;
set score(int value) {
if (value < 0 || value > 50) throw RangeError.range(value, 0, 50, 'score');
_score = value;
}
}
void main() {
var zayan = ScoreCard();
for (var value in [30, 60, 45]) {
try {
zayan.score = value;
} on RangeError catch (e) {
print(e);
}
}
print('Score: ${zayan.score}');
}RangeError (score): Invalid value: Not in inclusive range 0..50: 60
Score: 45Quick Revision
- What happens to the rest of a function after
throwruns? - Which built-in type fits "the object is not ready for this call"?
- Why does
?? throw ...need parentheses? - What return type does a function that always throws have?
- Why
implements Exceptionand notextends? - In which order do
onclauses for a parent and a child go?
Key Takeaways
throwstops the function and hands an object to the caller; uncaught, it ends the program.ArgumentError,RangeError,StateError, andFormatExceptioncover common problems.throwis an expression: use it after??in parentheses, or as an=>body.- A function that always throws returns
Never, and Dart promotes variables after calling it. - Custom exceptions
implements Exception, carry details infinalfields, and overridetoString; a shared parent lets callers catch the whole family. - Throwing from constructors and setters keeps invalid objects from ever existing.
What's Next?
Lecture 086 — Rethrowing, Exception Design & Assertions. Sometimes a function must catch
an exception, log it, and still pass it on. Next: rethrow, designing exceptions for a whole
program, and assert for checking your own assumptions during development.