error_trace 1.0.1 copy "error_trace: ^1.0.1" to clipboard
error_trace: ^1.0.1 copied to clipboard

Utilities to preserve stack traces across asynchronous calls for better debugging.

Dart utilities to preserve stack traces across asynchronous calls for better debugging.

With error_trace you can get full stack traces that include the whole chain of calls, even if there are asynchronous gaps in the middle. This makes it easier to debug Dart and Flutter applications.

Background: Problems with exceptions in asynchronous calls

Debugging errors in asynchronous Dart code can be tricky. When an error occurs within an async function, the resulting stack trace may be incomplete. This is because asynchronous calls introduce "gaps" where the execution pauses and resumes. Across these gaps, the original call stack can be lost, making it difficult to trace the error back to its source.

Features #

Use this package to:

  • Chain errors: Throw exceptions that wrap the original error, preserving the context of the initial failure across asynchronous calls.
  • Preserve stack traces: Maintain complete stack traces across async, await, and Future gaps, preventing loss of crucial call stack information.
  • Enhance crash reporting: Report crashes with full, chained stack traces to services like Crashlytics or Sentry for better debugging.
  • Format error output: Print chained errors in a clear, readable format, making it easier to understand the error sequence.
  • Improve debugging: Quickly pinpoint the root cause of errors, even in complex asynchronous workflows.

Getting started #

First, add error_trace package to your pubspec dependencies.

After that, you can import the library and use it:

import 'package:error_trace/error_trace.dart';

Usage #

Consider the following Dart code:

Future<void> fetchData() async {
  await Future.delayed(Duration(milliseconds: 100));
  throw Exception('Failed to fetch data');
}

Future<void> processData() async {
  await fetchData();
}

void main() {
  processData().catchError((error, stackTrace) {
    print('Caught an error:\n$error\n');
    print('Stack trace:\n$stackTrace');
  });
}

When you run this code, you might expect the stack trace to show that the error originated in fetchData and was called by processData. However, the output might look something like this:

Caught an error:
Exception: Failed to fetch data

Stack trace:
#0 fetchData.<anonymous closure> (file:///path/to/your/file.dart:2:9) <asynchronous suspension>
#1 Future.Future.microtask.<anonymous closure> (dart:async/future_patch.dart:187:31) <asynchronous suspension>

Notice that the stack trace doesn't show that processData called fetchData. The asynchronous gap introduced by await Future.delayed has caused the loss of that part of the call stack. This makes it harder to understand the sequence of events that led to the error.

To preserve the complete stack trace across asynchronous calls we transform the above code by using error_trace in the following way:

Future<void> fetchData() async {
  await Future.delayed(Duration(milliseconds: 100));
  throw Exception('Failed to fetch data');
}

Future<void> processData() async {
  try {
    await fetchData();
  } catch (e, st) {
    // Throw a TraceableException that includes the cause exception details
    throw TraceableException(e, st, message: 'Process data exception');
  }
}

void main() {
  processData().catchError((error, stackTrace) {
    print('Caught an error:');
    // Use printError to print the chain of errors with the complete stack trace
    printError(error, stackTrace);
  });
}

Now, the output might look something like this:

Caught an error:
Process data exception (Caused by: Exception: Failed to fetch data)
  path/to/your/file.dart 6:13  processData
  path/to/your/file.dart 14:5  main
Caused by: Exception: Failed to fetch data
  path/to/your/file.dart 1:9  fetchData

Notice that the stack trace now shows that processData called fetchData.

You can check trace_errors_from_async_function_example.dart for a more detailed example.

Chaining exceptions #

To prevent the loss of the stack trace you have to catch the exceptions that are thrown by other parts of the code and throw a Traceable that wraps the cause exceptions.

Traceable is an interface that you can implement to create your own traceable exceptions or errors. It has two properties:

  • causeError: The original error that caused this exception or error.
  • causeStackTrace: The stack trace of the original error.

TraceableException and TraceableError are ready-to-use classes that implement the Traceable interface. These classes extend Exception and Error, respectively:

Future<void> main() async {
  try {
    await operationThatMayThrow();
  } on Exception catch (e, st) {
    throw TraceableException(e, st);
  } on Error catch (e, st) {
    throw TraceableError(e, st);
  }
}

Also, you can extend these classes if you want to implement your own Traceable exceptions or errors:

class MyException extends TraceableException {
  MyException(
    super.causeError, // The original error that caused this exception.
    super.causeStackTrace, // The stack trace of the original error.
  ) : super(name: 'MyException');
}

// Or

class MyError extends TraceableError {
  MyError(
    String message, // A message describing the error.
    super.causeError, // The original error that caused this error.
    super.causeStackTrace, // The stack trace of the original error.
  ) : super(name: 'MyError', message: message);
}

There is more info about theses classes in the API reference.

Chaining using Future callbacks

Use Future.catchError to chain exceptions in a code that doesn't use async and await:

Future<void> processData() {
  return fetchData().catchError((e, st) {
    throw TraceableException(e, st, message: 'Process data exception');
  });
}

You can check trace_errors_of_unawaited_future_example.dart for a more detailed example.

Printing errors #

The error_trace package provides a convenient way to print chained errors with their complete stack traces using the printError function. This function recursively traverses the chain of Traceable exceptions or errors, printing each one along with its stack trace in a clear, readable format.

Here's how you can use it:

void main() {
  runZonedGuarded(() {
    // Some operations that throw exceptions...
  }, (error, stackTrace) {
    print('Uncaught error:');
    printError(error, stackTrace);
  });
}

It prints the error details formated like this:

FooException: Foo failed (Caused by: BarException: Bar failed (Caused by: Exception))
  path/to/your/foo.dart 6:13   fooFunction
  path/to/your/file.dart 14:5  main
Caused by: BarException: Bar failed (Caused by: Exception)
  path/to/your/bar.dart 3:3   someFunction
  path/to/your/bar.dart 10:6  barFunction
Caused by: Exception
  path/to/your/another.dart 3:15  anotherFunction

You can check the API reference for additional formating tools.

Reporting errors #

The error_trace package makes it easy to report chained errors to services like Firebase Crashlytics or Sentry.

Reporting errors to Crashlytics

You can use chainCauses with Crashlytics in the following way:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp();

  FlutterError.onError = (errorDetails) {
    FirebaseCrashlytics.instance.recordFlutterFatalError(
      errorDetails.exception,
      // Creates a [Chain] instance that includes all the causes chained
      errorDetails.stack.chainCauses(error),
    );
  };

  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(
      error,
      // Creates a [Chain] instance that includes all the causes chained
      stack.chainCauses(error),
      fatal: true,
    );
    return true;
  };

  // ...
}

The chainCauses extension method on StackTrace combines multiple stack traces from a chain of errors into a single Chain object. This object is provided by the stack_trace package and can be passed to Crashlytics because it implements StackTrace.

You can check a full example at report_errors_to_crashlytics_example.dart.

More info on how to setup Crashlytics can be found here.

Reporting errors to Sentry

You can provide an ExceptionCauseExtractor that extracts causes from Traceable errors in the following way:

class _TraceableExtractor extends ExceptionCauseExtractor<Traceable>  {
  @override
  ExceptionCause? cause(Traceable error) {
    return ExceptionCause(error.causeError, error.causeStackTrace);
  }
}

Future<void> main() async {
  await SentryFlutter.init(
    (options) {
      options.addExceptionCauseExtractor(_TraceableExtractor());
    },
    // Init your App
    appRunner: () => runApp(MyApp()),
  );
}

More info on how to setup Sentry can be found here.

Additional information #

Under the hood, this package uses the stack_trace package to manipulate and format stack trace. Take a look on it if you want to make more advanced features.

You will notice that stack_trace provides Chain.capture that also solves the problem of losing stack trace information in asynchronous operations. It's a good solution, however, it's not recommended for production code because the performance overhead of creating new zones to capture the stack trace.

1
likes
160
points
27
downloads

Publisher

unverified uploader

Weekly Downloads

Utilities to preserve stack traces across asynchronous calls for better debugging.

Repository (GitHub)
View/report issues

Topics

#error #error-handling #stacktrace #debug #exceptions

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

meta, stack_trace

More

Packages that depend on error_trace