skip to content

In Dart, how do you define a custom exception class, and why implement Exception rather than throw a plain String?

level: middleimportance: should knowfreq 44%

answer

  1. any non-null object is throwable
  2. implements, not extends
  3. fields a caller can read
  4. override toString
  5. only_throw_errors lint

basics

~20 s

Dart can throw any non-null object, but a custom exception should be a class that implements Exception, carries fields describing the failure and overrides toString, so callers can catch it precisely with an on clause and read its data.

solid answer

~40 s

Dart lets you `throw` any non-null object, including `'Out of stock'` or `42`, but a string gives callers nothing to filter on except `on String` and nothing structured to read. The idiom is a small class that `implements Exception`, holds `final` fields such as the row and column, has a `const` constructor and overrides `toString()`. Callers then write `on RowException catch (e)` and read `e.row`. It must be `implements`: `Exception` is an `abstract interface class` with only a factory constructor, so `extends Exception` does not compile. `Exception('message')` works but builds a private `_Exception` callers cannot target, which the SDK discourages in library code. For bugs, reuse `ArgumentError` or `StateError`, or `extends Error` so the stack trace is recorded. The opt-in `only_throw_errors` lint rejects throwing anything else.

code

dart · 26 lines
dart
class RowException implements Exception {
  const RowException(this.row, this.column, this.message);

  final int row;
  final String column;
  final String message;

  @override
  String toString() => 'RowException(row $row, $column): $message';
}

int quantityAt(int row, String cell) {
  final value = int.tryParse(cell);
  if (value == null) {
    throw RowException(row, 'quantity', 'not an integer: "$cell"');
  }
  return value;
}

void main() {
  try {
    quantityAt(7, 'seven');
  } on RowException catch (e) {
    print('fix row ${e.row}, column ${e.column}'); // fix row 7, column quantity
  }
}

go deeper

for a junior

Recall that any non-null object can be thrown, but real code throws a class that implements Exception and carries fields.

for a middle

Explain why extends Exception fails, what Exception('msg') returns, and why a toString override and typed fields matter to callers.

for a senior

Show where you draw the line between domain exceptions and Error types, and how you keep the cause and trace when translating one into another.

for a principal

Decide how a codebase names and groups its exception types so public APIs document their throws and lints keep stray strings out.

## What Dart lets you throw A Dart **throw expression** accepts any value whose type is assignable to `Object` — that is, anything **non-null**. All of these compile: ```dart throw 'Out of stock'; throw 42; throw FormatException('Expected 2 cells'); ``` Two things do not compile: `throw null`, and throwing an expression of nullable type such as a `String?` (the analyzer reports `throw_of_invalid_type`). For the same reason an `on` clause cannot name a nullable type. Before Dart 3, a thrown `null` surfaced as `NullThrownError`; Dart 3.0 removed that class because sound null safety makes it impossible. Because `throw` is an expression, it also works in arrow bodies: `int get total => throw UnimplementedError();`. ## Why a String is a poor exception - **Nothing to filter on.** A caller can only write `on String` or a catch-all, and `on String` also matches any other string anyone throws. - **Nothing to read.** The row number, the column and the bad value are baked into prose, so the caller must parse your message. - **No intent.** A reader cannot tell whether the throw reports a bug or an expected failure. - **Lints object.** The `only_throw_errors` lint — not in the recommended set, but common in stricter configurations — flags any thrown value that is not an `Exception` or `Error`. ## Writing a custom exception The house pattern has four parts: 1. `implements Exception`, which documents that callers are expected to catch it; 2. `final` fields with the data a handler needs; 3. a `const` constructor, since the object is immutable; 4. a `toString()` override, because that is what logs and uncaught-error reports print. ```dart class RowException implements Exception { const RowException(this.row, this.column, this.message); final int row; final String column; final String message; @override String toString() => 'RowException(row $row, $column): $message'; } ``` A caller can now write `on RowException catch (e)` and use `e.row` without touching unrelated exceptions. ## Why implements and not extends `Exception` is declared as an **`abstract interface class`** whose only constructor is a **factory**. That rules out `extends Exception` twice over: the `interface` modifier forbids extending it outside `dart:core`, and a subclass would have no generative superclass constructor to call. `implements Exception` is the only way in, and the interface has no members to implement. `Exception('message')` compiles, but the factory returns a private `_Exception` whose `toString()` is `Exception: message`. Callers can catch it only with `on Exception`, which also catches every other exception, so the SDK discourages it in library code and suggests it only for tests or quick development. ## When the failure is a bug instead | Situation | Throw | |---|---| | a bad argument the caller could have checked | `ArgumentError` / `ArgumentError.value` / `RangeError` | | a method called in the wrong state | `StateError` | | an operation the object does not support | `UnsupportedError` | | a method not written yet | `UnimplementedError` | | none of those fit | your own class that **extends `Error`** | Extending `Error` — `Error` has a public generative constructor — matters because only classes that **extend** it get `stackTrace` filled in on the first throw. Custom `Error` subclasses are rare; the built-in ones cover most preconditions. ## Checklist for a reviewer - Does every thrown domain failure have its own type a caller can name in `on`? - Does it carry the data the handler needs as fields, not only as a message? - Is `toString()` readable in a log line? - Are programming mistakes reported with `Error` types instead of custom exceptions?

  • In Dart, why does a custom Error subclass normally extend Error instead of implementing it?
    Only classes that extend `Error` have their `stackTrace` recorded automatically on the first throw, and `Error` has a public generative constructor to call. Implementing it means supplying a `stackTrace` getter yourself; `StackOverflowError` and `OutOfMemoryError` do that because they are special runtime errors.
  • In Dart, should a custom exception hold the original exception it was translated from?
    Often yes: a field such as `final Object? cause` lets a handler or log show the low-level `FormatException` behind a `RowException`. Pair it with the original stack trace — pass it along or throw with `Error.throwWithStackTrace` — so the log still points at the real throw site.

saying these in an interview costs you the question

  • Custom exceptions in Dart should use extends Exception.
  • Dart can only throw objects of Exception or Error types.
  • Exception('bad row') gives callers a precise type to catch.
  • Domain failures such as a bad CSV row should extend Error.
  • throw null is allowed and caught as a null e.