DEV Community

Nithin
Nithin

Posted on

Dart and Exception Handling: No Guardrails, Only Discipline

Have you ever relied on Dart’s throw and thought, “Great—now my callers are forced to handle this”? Think again. In Java and its cousins, exception chaining is a first-class citizen: you catch, chain, and let the compiler enforce your discipline. In Dart (and by extension Flutter), you’re on your own.

Exception Chaining in Java: A Quick Recap

Java’s checked exceptions bite when they must. If a method declares throws IOException, every caller up the chain either handles it or propagates it. And when you rethrow, you do it right:

try {
  riskyOperation();
} catch (IOException e) {
  throw new MyAppException("failed to do the thing", e);
}
Enter fullscreen mode Exit fullscreen mode

That little , e preserves the stack trace and makes debugging a dream. Learn more on exception chaining Wikipedia.

Dart’s “Freedom” Feels a Lot Like No Guardrails

Dart’s type system doesn’t distinguish between checked and unchecked exceptions—all are unchecked. You can write:

void doSomethingRisky() {
  throw StateError('something went terribly wrong');
}
Enter fullscreen mode Exit fullscreen mode

…and your callers aren’t even reminded to catch it. There’s no compiler error, no warning—nothing. You have to remember, by convention, that doSomethingRisky() might explode.

Even Result<T> Patterns Don’t Save You

“Okay,” you say, “I’ll wrap everything in a Result<T> so that forgetting to handle it is impossible.” Not quite. In Dart:

final result = await getUser();
Enter fullscreen mode Exit fullscreen mode

There’s… nothing stopping you from parking result in a corner, ignoring its .isSuccess, and then accessing .value. At runtime? Boom—a throw you forgot to anticipate. You traded one potential uncaught exception for another.

Catching Errors (If You Care)

A good rule of thumb is “catch if you can, otherwise let it bubble”—but since Dart never forces you to catch in the first place, the habit rarely forms. Consider these behaviors:

Dart Effect
No catch Bubbles up silently—no compile-time reminder
catch (e) { rethrow; } Bubbles up with original stack trace
catch (e) { throw e; } Bubbles up losing original stack trace

Nullable Responses: The Invisibility Cloak

Returning T? or Future<T?> might seem like a gentle way to sidestep exceptions:

Future<User?> fetchUser() async {
  try {
    return await api.getUser();
  } catch (e) {
    log(e);
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

But now, instead of an explicit error, all you have is a silent null. The real crime? You’ve lost the ability to bubble up the original error. The stack trace, the exception type, the context—all vanished in the void. Without that, diagnosing failures becomes a wild goose chase.

A Pragmatic Prescription

  1. Be deliberate. Whether you throw or return null (or wrap in Result), pick a convention and document it.
  2. Catch at the edges. Localize unchecked exceptions at boundaries—UI layer, service layer—where you can log and recover.
  3. Preserve your traces. If you catch and rethrow, use rethrow so the original stack isn’t lost.
  4. Enforce via tooling. Automate linting to flag un-caught futures or raw throw without surrounding try/catch.
  5. Educate your team. Habits form from code reviews: make error-handling expectations crystal clear.

Dart may not force error handling—but with discipline, conventions, and the right practices, you can. Catch if you can; otherwise, be ready to bear the pain.

Top comments (0)