Dart Flutter Article

Anti-patterns of error handling in dart

This article will show common mistakes you can make when handling exceptions and alternatives to how to do it right.
Plague Fox 4 min read
Anti-patterns of error handling in dart
Photo by Nong V / Unsplash

This article will show common pitfalls you can make when handling exceptions and how to do it right. The article will help to avoid several popular misconceptions associated with the interception of errors. These misconceptions are mainly related to the practices of other languages and do not consider Dart's specifics.

Most bugs are related to the loss of the stack trace, and without it, the error logs in the release (for example, Crashlytics or Sentry) are practically useless, and it tends to be very difficult to trace the root of the problem.


Problems and common mistakes

First of all, return null on exception - worst option ever.

try {
  final json = await client.get(id);
  return Item.fromJson(json);
} on Object {
  return null;
}

You lose exception and the stack trace, you can’t respond properly with the user interface, and you won’t know in Crashlytics that something bad happened.

Using unions to catch errors like this:

try {
  final json = await client.get(id);
  final item = Item.fromJson(json);
  return DataOrException.data(item);
} catch (error) {
  return DataOrException.failure(error);
}

Well, now you “rethrow” the exception but lost stack trace again. If you try adding a stack trace, you will have problems combining it together. And without a stack trace, you simply cannot trace the roots of the problem since the stack trace is even more important than the exception itself. This approach doesn't make sense in Dart since the stack trace is separate from the exception.

And the last popular misconception is throwing another exception using "throw"

try {
  final json = await client.get(id);
  return Item.fromJson(json);
} on Object {
  throw ApiException();
}

And again, you lost the existing stack trace, and after that, it is very problematic (almost impossible) to track problems in Crashlytics or Sentry without the source stack trace.


Tips and solutions

Don't be afraid of exceptions.
It's much worse to silence a mistake than to let it pass.
No need to try to foresee everything.
You will learn the mistakes that must be foreseen and properly processed from the Crashlytics or even at the self-test stage.

Just don't catch them where they don't belong.

  final json = await client.get(id);
  return Item.fromJson(json);

also, you can rethrow source exception

try {
  final json = await client.get(id);
  return Item.fromJson(json);
} on Object {
  rethrow;
}

If you need to replace the original error, don't forget the stack trace. Use throwWithStackTrace for this

try {
  final json = await client.get(id);
  return Item.fromJson(json);
} on Object catch (_, stackTrace) {
  Error.throwWithStackTrace(
    ApiException(‘Something goes wrong’),
    stackTrace,
  );
}

If you think you can recover from the panic of a specific exception - try to catch specific types of errors that you can predict

try {
  final json = await client.get(id);
  return Item.fromJson(json);
} on FormatException {
  return const Item.empty();
}

Don't forget that you can catch various actions, and the try-catch-finally syntax is as follows

try {
  ...
} on TimeoutException {
  ...
} on HandshakeException catch (error, stackTrace) {
  ...
  Error.throwWithStackTrace(
    ApiException(‘Something goes wrong’),
    stackTrace,
  );
} on Object {
  ...
  rethrow;
} finally {
  ...
}

Use zones to catch asynchronous errors without awaiting

runZonedGuarded<void>(() async {
    longAsyncOperation();
    runApp(App());
  },
  (error, stackTrace) => ...
);

Future objects have an ignore method if you really don't care if the method succeeds or not.

FirebaseAnalytics.instance.logAppOpen().ignore();
FirebaseCrashlytics.instance.recordError(exception, stackTrace).ignore();

Stacktrace is a lifesaver when investigating bugs. Learn how to interact with it effectively. For example, you can get the current stack trace and add additional information to it.

StackTrace.fromString('${StackTrace.current}\n'
    'Headers: "${jsonEncode(response.headers)}"');

Look at the stack_trace library. It simplifies the interaction with the stack trace and allows you to make it more capacious and beautiful.


Afterword

Don't be afraid to write buggy code. Be aware of unsupported code with floating bugs, silent exceptions, and lost stack traces.
You still can't catch all the bugs at the development stage. Especially if you remember that there are deadlines and there is not always a separate design for displaying errors. Some bugs can only be reproduced on certain devices, manufacturers, and versions. I bet most of the developers won't be able to name half of the exceptions you can get with a normal HTTP request. Just don't worry, let the error happens, and log everything.

Well, also, not every error is a bug, for example, if you try to send an email when the Internet is not available, this is not a bug, but if there are many such cases, it may need to be beaten from the point of view of the interface or implement a new feature of delayed sending. You will definitely want to implement this feature if you see many of these errors. And if you do not throw this error, you will not learn about the user's needs and can't subsequently improve a user experience.

But since a developer cannot predict all exceptions and process them correctly, such a developer simply writes a common handler for all errors. It doesn't matter, it's a problem with the cache, the Internet, the device, the business logic, the operating system, the updated backend, timeout, handshake, the lack of rights, etc.

And since you can't predict a particular problem, you can't handle it correctly either. Incorrect handling of the problem itself becomes an even greater problem.

Consequently, this leads to the "silencing" of exceptions and a serious deterioration in the quality of the application.
The product owners and the developer himself do not know about the problems.

Share
Comments
More from Plague Fox
Microbenchmarks are experiments
Dart Flutter Article

Microbenchmarks are experiments

Benchmarks are not just about numbers—they are experiments that need interpretation. This post dissects a Dart vs JavaScript microbenchmark, illustrating why cool animations often mask the real value: insightful analysis. Numbers without context are just as meaningful as numerology
Vyacheslav Egorov 12 min read

Plague Fox

Engineer by day, fox by night. Talks about Flutter & Dart.

Great! You’ve successfully signed up.

Welcome back! You've successfully signed in.

You've successfully subscribed to Plague Fox.

Success! Check your email for magic link to sign-in.

Success! Your billing info has been updated.

Your billing was not updated.