skip to content

In Dart, how does rethrow differ from throw e inside a catch, and when do you need Error.throwWithStackTrace?

level: middleimportance: should knowfreq 38%

answer

  1. which stack trace survives
  2. throw e restarts the trace
  3. rethrow only inside catch
  4. throwWithStackTrace returns Never
  5. translate the exception, keep the trace

basics

~20 s

Inside a Dart catch, rethrow re-throws the caught object with its original stack trace, while throw e starts a new trace at the catch. Error.throwWithStackTrace (Dart 2.16+) throws any object with a trace you supply, for translating exceptions or throwing outside a catch.

solid answer

~40 s

`rethrow` is a statement that re-throws the object the enclosing catch clause caught, keeping the original stack trace, so the next handler still sees where the failure started. `throw e` throws the same object as if for the first time, so the trace a caller's `catch (e, st)` receives points at your catch block and the real origin is lost — the `use_rethrow_when_possible` lint in the recommended set flags it. `rethrow` only compiles lexically inside a catch clause and only re-throws the same object. `Error.throwWithStackTrace(error, stackTrace)`, added in Dart 2.16 and typed `Never`, fills both gaps: it throws any object — for example a new `ImportException` wrapping the low-level one — with a `StackTrace` you pass in, and it works outside a catch, such as when an error was stored and is re-raised later.

code

dart · 17 lines
dart
class RowException implements Exception {
  const RowException(this.row, this.message);
  final int row;
  final String message;

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

DateTime parseDate(int row, String cell) {
  try {
    return DateTime.parse(cell);
  } on FormatException catch (e, st) {
    // New, more useful exception; original trace still points into DateTime.parse.
    Error.throwWithStackTrace(RowException(row, e.message), st);
  }
}

go deeper

for a junior

Recall that rethrow keeps the original stack trace and throw e does not, and that rethrow only works inside a catch.

for a middle

Explain what each tool does to the object and the trace, and when Error.throwWithStackTrace replaces rethrow, including its Never return type.

for a senior

Show how you translate low-level exceptions into domain types at a layer boundary while keeping the original trace for whoever reads the logs.

for a principal

Set a convention for where exceptions get translated and how the cause and trace travel, so logs from every layer lead back to the real throw site.

## Three ways to throw again When a catch clause decides it cannot fully handle what it caught, it has three tools. They differ in **which object** is thrown and **which stack trace** travels with it. | Tool | Object thrown | Stack trace the next handler sees | Where it is allowed | |---|---|---|---| | `rethrow;` | the caught object | the **original** trace | only inside a `catch` clause | | `throw e;` | the caught object | a **new** trace from this line | anywhere | | `Error.throwWithStackTrace(x, st)` | any non-null object `x` | the trace **you pass** | anywhere | ## rethrow: partial handling The common pattern is **handle a little, then let it go**: log, count, clean up, and pass the failure on. ```dart try { importFile(path); } on FileSystemException catch (e) { print('import of $path failed: ${e.message}'); rethrow; // callers still see the original trace } ``` Key facts: - `rethrow` is a **statement**, not an expression, and takes no operand. - It must appear **inside a catch clause**; anywhere else the analyzer reports `rethrow_outside_catch`. A `finally` block does not count. - It re-throws **exactly the object that was caught**, so it cannot change the type. ## Why throw e is a trap `throw e` looks equivalent, but a throw records the stack at the point of the throw. The handler above you receives a trace that starts in your catch block, and the frames that show where the failure really happened are gone. Effective Dart's rule is **use `rethrow` to rethrow a caught exception**, and the `use_rethrow_when_possible` lint, part of the recommended and flutter sets, reports `throw e` inside a catch that could have been `rethrow`. One nuance for `Error` subclasses: a class that **extends** `Error` has its `stackTrace` field set only the **first** time it is thrown, so that field keeps the original location even after `throw e`. That does not help handlers that only look at the `st` parameter of their catch, and most of them do. ## Error.throwWithStackTrace: throw something new, keep the old trace Added in **Dart 2.16**, `static Never throwWithStackTrace(Object error, StackTrace stackTrace)` behaves like `throw error` would if the current stack trace were `stackTrace`. Its return type is `Never`, so the analyzer knows code after it is unreachable, and it can end a function whose return type is non-nullable. It solves two problems `rethrow` cannot: 1. **Translating an exception.** A layer that catches a low-level `FormatException` often wants to throw its own domain exception carrying context — the row, the file name — without losing where the parse failed: ```dart } on FormatException catch (e, st) { Error.throwWithStackTrace(RowException(row, 'date', e.message), st); } ``` 2. **Re-throwing outside a catch.** When code caught a failure earlier, stored the object and its trace, finished some other work, and now needs to fail with the original trace. Details worth knowing: - If the object **extends `Error`** and has never been thrown, its `stackTrace` field is set to the trace you pass. - The SDK does **not guarantee object identity** of the trace: a handler may receive a different `StackTrace` object with the same `toString()` contents. - Both arguments must be non-null. ## Choosing between them - Same object, inside the catch: **`rethrow`**. - Different object, or no longer inside the catch: **`Error.throwWithStackTrace`** with the trace captured by `catch (e, st)`. - Plain `throw e` of a caught object: almost never. Throwing a fresh object with a fresh trace is right only when the new throw site genuinely is where the failure should be reported from.

  • In Dart, why can a function returning DateTime end with Error.throwWithStackTrace in its catch clause and no return?
    `Error.throwWithStackTrace` is declared to return `Never`, so flow analysis knows the call never completes normally. The catch clause therefore cannot fall off its end, and the function needs no `return` there to satisfy its non-nullable return type.
  • In Dart, can rethrow be used inside a closure declared in a catch block, such as a callback passed to a logger?
    No. `rethrow` must be lexically inside the catch clause of the same function; a closure body is a separate function, so the analyzer reports `rethrow_outside_catch`. Capture `e` and `st` and use `Error.throwWithStackTrace(e, st)` inside the closure instead.

saying these in an interview costs you the question

  • throw e and rethrow are identical because the same object is thrown.
  • rethrow can re-throw a different exception object than the one caught.
  • rethrow works in a finally block after the catch has run.
  • Error.throwWithStackTrace only accepts objects that extend Error.
  • Translating an exception into a domain type always loses the original trace.