← Back to list

18.Error Handling

When beginning a project, it is common to code for the “happy path,” which assumes that everything will function perfectly.

Aly Route · 2026-04-12 20:09 · 0 claps · 11.3 min read
#error-handling #dart #dart-programming-language
Open on Medium ↗
Wiki topics: 💻 · Programming

18.Error Handling

When beginning a project, it is common to code for the “happy path,” which assumes that everything will function perfectly.

In reality code is unforgiving while a reader might overlook a typo, a program will not ignore a mistake.

The nature of a programmer’s work means that even small mistakes can have significant impacts. Unlike other professions where errors might be minor, a programmer’s mistake often results in the entire app crashing.

The goal of learning error handling is to learn how to “crash a little less” by anticipating failures.

So Before you learn how to handle errors, though, you get to make them!

· How to Crash an App?Dividing by ZeroNo Such MethodFormat ExceptionAssertionError · Reading Stack Traces · DebuggingWriting Some Buggy CodeAdding a BreakpointRunning in Debug Mode.Stepping Over the Code Line by LineWatching ExpressionsFixing the Bug · Handling ExceptionsError vs ExceptionCatching ExceptionsHandling Specific ExceptionsHandling Multiple ExceptionsThe Finally BlockThrow ExceptionWriting Custom Exceptions · Test Your Knowledge

How to Crash an App?

Errors indicate something went wrong with the program logic itself. They signal bugs that should be fixed, not caught.

Dividing by Zero

void main(){

1 ~/ 0; 

}

// Run Without Debugging 
// You will get
/*
Unhandled exception:
IntegerDivisionByZeroException
*/

IntegerDivisionByZeroException is Dart’s name for what happened. An exception is something outside of the usual rules. Dividing by an integer is normal, but dividing by zero is an exceptional case. Even though it’s exceptional, you’re still expected to know and plan for it.

Not handling an exception is an error. And errors crash your app.

No Such Method

void main(){

dynamic x = null;
print(x.isEven);

}

// Run Without Debugging 
// You will get
/*
Unhandled exception:
NoSuchMethodError: The getter 'isEven' was called on null.
*/

You’ll learn about debugging later , but until directed to do differently, run all the examples here without debugging

Unlike integers, null doesn’t have an isEven getter method.

The error you got here was a runtime error. Dart didn’t discover it until you ran the code, changing dynamic to int you will have a compile-time error.

A value of type ‘Null’ can’t be assigned to a variable of type ‘int’.

A value of type ‘Null’ can’t be assigned to a variable of type ‘int’.

Compile-time errors are much better than runtime errors because they’re obvious and immediate.

Format Exception

Another way to crash your app is to try to turn a non-numeric string into an

integer:

void main() {
int.parse('42'); // OK
int.parse('hello'); // FormatException
}

Dart has no idea how to convert the string ‘hello’ into an integer, so it stops executing the program with the following error:

Unhandled exception:
FormatException: Invalid radix-10 number (at character 1)
hello

AssertionError

During development, use an assert statement to disrupt normal execution if a boolean condition is false.

assert(<condition>, <optionalMessage>);
  • If condition is truenothing happens, continues normally
  • If condition is falsethrows AssertionError immediately
// Make sure the variable has a non-null value.
assert(text != null);

// Make sure the value is less than 100.
assert(number < 100);

// Make sure this is an https URL.

void setAge(int age) {
  assert(age >= 0, 'Age must not be negative');  // guard
  print('Age set to $age');
}

void main() {
  setAge(25);   // ✅ passes
  setAge(-1);   // 💥 AssertionError: Age must not be negative
}

In production code assertions are ignored and the arguments to assert aren't evaluated.

Reading Stack Traces

A stack trace is a printout of all the methods on the call stack when an error occurs. In the stack trace above, most methods are internal. #4 main is the only one that’s part of your code.

Computers use Stack Data Structure to keep track of the current method being executed.

void main() {
functionOne();
}

void functionOne() {
functionTwo();
}

void functionTwo() {
functionThree();
}

void functionThree() {
int.parse('hello');
}

When Dart executes this program, it’ll start by calling main . Because main is the current function, Dart adds main to the call stack.Then, main calls functionOne , so Dart puts functionOne on the call stack.FunctionOne calls functionTwo , and functionTwo calls functionThree . Each time you enter a new function Dart adds it to the call stack.

Normally, when functionThree finishes, Dart would pop it off the top of the stack, go back to functionTwo , finish functionTwo , pop it off the stack and so on until main finishes. However in this case, there’s about to be a tragedy in functionThree , which will bring everything to a grinding halt.

Run the code you just wrote. The app crashes when you reach the line int.parse(‘hello’);

Look at the debug console, and you’ll see the stack trace that shows the call stack at the time of the crash.

There your four methods sit in the middle of the stack. The other methods above and below them are internal to Dart. On the right side, you can see bin/starter.dart followed by a line number.

Click the one after functionThree , and VS Code brings you to the line number where the crash occurred in functionThree , at int.parse(‘hello’);

Debugging

It’s not always obvious from the stack trace where the bug in your code is. VS Code has good debugging tools to help you out in this situation.

Writing Some Buggy Code

void main() {
  final characters = ' abcdefghijklmnopqrstuvwxyz';
  final data = [4, 1, 18, 20, 0, 9, 19, 0, 6, 21, 14, 27];
  final buffer = StringBuffer();

  for (final index in data) {
  final letter = characters[index];
  buffer.write(letter);
  }

  print(buffer);
}

First, run the code without debugging. You’ll get a crash with the following error message:

what does that mean?

The stack trace tells you the error happened in the main method on line 6, which is the following line:

final letter = characters[index];

That line looks OK — no division by zero or trying to parse a weird string.

To find the error, you’ll use the debugging tools available in VS Code and step through your code line by line.

Adding a Breakpoint

Click the margin to the left of line 2. This will add a red dot:

That red dot is called a breakpoint. When you run your app in debug mode, execution will pause when it reaches that line.

Running in Debug Mode.

  • Choose Run ▸ Start Debugging from the menu.
  • Click Debug above the main method

  • In the top-right of the window, click the dropdown menu next to the Run button and make sure it says Start Debugging

Stepping Over the Code Line by Line

VS Code pauses execution at line 2. Then, a floating button bar pops up with various debugging options. If you hover your mouse over each button, you can see what it does.

  • Continue: It resumes normal execution until the next breakpoint, if any, is reached.
  • Step Over: Pressing it executes one line of code but doesn’t descend into the body of any function it reaches.
  • Step Into is a command used to enter the body of a function or method being called on the current line of code.
  • Step Out is a command used to finish the execution of the current function and return immediately to the caller.

Press the Step Over button several times until execution reaches line 7: buffer.write(letter); , then Look at the Run and Debug panel on the left. The Variables section shows the current values of the variables in your code.

Keep pressing Step Over for a few more iterations of the for loop while keeping an eye on the values of the variables. You’ll begin to see the pattern of how the code works.

Watching Expressions

When you tire of stepping one line at a time through the for loop, add a breakpoint to line 6: final letter = characters[index]; .

Then, find the Watch section on the Run and Debug panel. Add the following two expressions by pressing the + button:

  • characters[index]
  • buffer.toString()

After that, press the Continue button a few times, keeping an eye on the expressions you’re watching on the left.

Fixing the Bug

When the app finally crashes, what’s the value of index ?

It’s 27 . What’s the length of the characters string? If you don’t want to count, add characters.length to the Watch window. It’s also 27 .

Ah, that’s it! You recall that lists and string indexes are 0-based, so index 27 is one greater than the last position in the list. That was causing the range error.

// replace line 2 with the following:
final characters = ' abcdefghijklmnopqrstuvwxyz!';

Now, rerun the code without debugging.

No errors this time! You see the following output in the debug console:

// dart is fun!

If you want to remove the breakpoints, click the red dots on the left of lines 2 and 6.

Handling Exceptions

Exceptions represent anticipated failures that can happen during normal operation and should be caught and handled.

Error vs Exception

Something went wrong in your Dart program, Was it YOUR fault as a programmer?

  • YES => Error (fix your code, don’t catch it)
  • NO => Exception (catch it and handle it)

Catching Exceptions

When connected to the internet, you can’t control what comes to you from the outside world. Invalid JSON doesn’t happen very often, but you should write code to deal with it when you get it.

try-catch block

  • You put the code that might throw an exception in the try block “throw”, but the meaning is “cause”. If it does throw, the catch block will handle the exception without crashing the app
  • e is the error or exception. You can use catch (e, s) instead if you need the stack trace, s being the StackTrace object.
import 'dart:convert';
void main() {

  const json = 'abc';

  try {
    dynamic result = jsonDecode(json);
    print(result);
  } catch (e) {
    print('There was an error.');
    print(e);
  }

}

/*
There was an error.
FormatException: Unexpected character (at character 1)
abc
^
*/

This time, the app didn’t crash, you handled this exception.

const json = '{"name":"bob"}';
/*This time, the try block finishes successfully, and you see the Dart map 
that jsonFormat produced: 
{name: bob}
*/

Handling Specific Exceptions

Using a catch block will catch every exception that happens in the try block, In fact, sometimes, it can make things worse because you’re hiding your problems rather than dealing with them.

You can’t handle every programming exception with one catch block. It’s better to focus on one problem at a time. To catch a specific exception, use the on keyword.

const json = 'abc';
try {
dynamic result = jsonDecode(json);
print(result);
} on FormatException {
print('The JSON string was invalid.');
}

The solution to no internet is quite different than the solution to a range error.

Handling Multiple Exceptions

When there’s more than one potential exception that could occur, you can use multiple on blocks to target them.

void main() {
  const numberStrings = ["42", "hello"];

  try {
  for (final numberString in numberStrings) {
    final number = int.parse(numberString);
    print(number ~/ 0);
  }

  } on FormatException {
    handleFormatException();

  } on UnsupportedError {
    handleDivisionByZero();
  }

  }

void handleFormatException() {
print("You tried to parse a non-numeric string.");
}

void handleDivisionByZero() {
print("You can't divide by zero.");
}
  • IntegerDivisionByZeroException is deprecated and will probably be removed from the language in the future. That doesn’t mean you’ll be able to divide by zero in the future. It just means you should call it UnsupportedError when catching such an exception.

The code in the try block terminates as soon as you hit the first error. You never made it to the format exception. But you were ready for it.

The Finally Block

There’s also a finally block you can add to your try-catch structure. The code in that block runs both if the try block is successful and if the catch or on block catches an exception.

void main() {
  final database = FakeDatabase();
  database.open();
  try {
  final data = database.fetchData();
  final number = int.parse(data);
  print('The number is $number.');
  } on FormatException {
  print("Dart didn't recognize that as a number.");
  } finally {
  database.close();
  }
  }

class FakeDatabase {
  void open() => print('Opening the database.');
  void close() => print('Closing the database.');
  String fetchData() => 'forty-two';
}

// output if fetchData => forty-two
/*
Opening the database.
Dart didn't recognize that as a number.
Closing the database.*/

// output if fetchData => 42
/*
Opening the database.
The number is 42.
Closing the database.*/

FakeDatabase represents a situation where you must clean up some resources even if the operation in the try block is unsuccessful. Note that you “close” the database in the finally block.

Throw Exception

Throw means "something went wrong, I'm stopping right here and reporting the problem."

Throw does 3 things the moment it runs:

  1. STOPS execution immediately at that line.
  2. CREATES the exception object.
  3. SENDS it up the call stack looking for a catch.
void divide(int a, int b) {
  if (b == 0) {
    throw Exception('Cannot divide by zero'); // 🛑 stop here
  }
  print(a ~/ b); // only runs if b != 0
}

void main() {
  divide(10, 2);  // ✅ prints 5
  divide(10, 0);  // 🛑 throws — execution stops
  divide(10, 5);  // ❌ never reached
}

/*
5
Unhandled exception: Exception: Cannot divide by zero  💥
*/

You can throw anything

throw Exception('a message');         // throw an Exception
throw FormatException('bad format');  // throw a specific Exception
throw StateError('wrong state');      // throw an Error
throw 'something went wrong';         // throw a plain String (allowed but bad practice)
throw 42;                             // throw a number (allowed but bad practice)

`=> throw` = raise the alarm**

`=> catch` = respond to the alarm**

Writing Custom Exceptions

You should use the standard exceptions whenever you can, but you can also define your own exceptions when appropriate.

class YourException implements Exception {
  final String message;

  YourException(this.message);

  @override
  String toString() => 'YourException: $message';
}
// ── Custom Exceptions ──────────────────────────────

class InsufficientBalanceException implements Exception {
  final double balance;
  final double amount;

  InsufficientBalanceException(this.balance, this.amount);

  @override
  String toString() =>
      'InsufficientBalanceException: '
      'tried to withdraw \$$amount but balance is \$$balance';
}

class AccountFrozenException implements Exception {
  final String accountId;

  AccountFrozenException(this.accountId);

  @override
  String toString() =>
      'AccountFrozenException: account $accountId is frozen';
}

class InvalidAmountException implements Exception {
  final double amount;

  InvalidAmountException(this.amount);

  @override
  String toString() =>
      'InvalidAmountException: \$$amount is not a valid amount';
}

// ── Bank Account ────────────────────────────────────

class BankAccount {
  final String id;
  double balance;
  bool frozen;

  BankAccount(this.id, this.balance, {this.frozen = false});

  void withdraw(double amount) {
    if (amount <= 0) {
      throw InvalidAmountException(amount);       // 🛑 bad amount
    }
    if (frozen) {
      throw AccountFrozenException(id);           // 🛑 account frozen
    }
    if (amount > balance) {
      throw InsufficientBalanceException(balance, amount); // 🛑 no money
    }
    balance -= amount;
    print('Withdrawn \$$amount — new balance: \$$balance');
  }
}

// ── Main ────────────────────────────────────────────

void main() {
  var account = BankAccount('ACC-001', 100.0);

  // Test 1 — invalid amount
  try {
    account.withdraw(-50);
  } on InvalidAmountException catch (e) {
    print(e); // InvalidAmountException: $-50.0 is not a valid amount
  }

  // Test 2 — not enough money
  try {
    account.withdraw(200);
  } on InsufficientBalanceException catch (e) {
    print(e);           // InsufficientBalanceException: tried to withdraw...
    print(e.balance);   // 100.0
    print(e.amount);    // 200.0
  }

  // Test 3 — frozen account
  account.frozen = true;
  try {
    account.withdraw(50);
  } on AccountFrozenException catch (e) {
    print(e); // AccountFrozenException: account ACC-001 is frozen
  }

// Test multiple custom exceptions
/*
void main() {
  var account = BankAccount('ACC-001', 100.0);

  try {
    account.withdraw(200);
  } on InvalidAmountException catch (e) {
    print('Fix the amount: $e');
  } on AccountFrozenException catch (e) {
    print('Unfreeze account: $e');
  } on InsufficientBalanceException catch (e) {
    print('Not enough money: $e');
  } catch (e) {
    print('Unknown error: $e');   // fallback
  }
}
*/
}

Test Your Knowledge


메타데이터
post_id
9d6a2c8c9ef6
slug
18-error-handling-9d6a2c8c9ef6
url
https://medium.com/@aly.route/18-error-handling-9d6a2c8c9ef6
canonical_url
https://medium.com/@aly.route/18-error-handling-9d6a2c8c9ef6
author_url
https://medium.com/@aly.route
status
ok
fetched_at
2026-07-17 14:47:18