errflow 0.4.0-nullsafety.0 copy "errflow: ^0.4.0-nullsafety.0" to clipboard
errflow: ^0.4.0-nullsafety.0 copied to clipboard

outdated

A package for making it somewhat easier to comprehend the flow of errors and handle them.

errflow #

Pub Version Dart CI

A Dart/Flutter package for making it somewhat easier to comprehend the flow of errors and handle them.


apologies for the breaking changes in v0.1.4.
Please see the Changelog for the list of the changes.
The package should be safer now in exchange for the inconvenience caused by them.


Motivation #

I made this package because I found it cumbersome to handle exceptions:

  • An app stops on an exception if it is not caught.
  • It is sometimes unclear if an exception has already been caught somewhere.
  • It is not preferable to catch an exception in a different layer that should be agnostic about a specific exception (e.g. a DB error / a network error).
  • However, it is difficult to return an error value (instead of the exception itself to avoid the above issue) together with the result of some processing from a method to its caller located in another layer.

Solutions:

  • Result in package:async
  • errflow (this package)

These look different, but roughly speaking, they are similar in that they provide a way to pass a result and an error value from a method to the caller. The former uses a notifier passed from a caller to notify values to the caller, and the latter returns a Result object that can hold either of those values.

A big difference is that this package also provides handlers and a logger to enable errors to be handled more easily in a unified manner.

Isn't it also cumbersome to have to pass a notifier?

It is probably possible to remove the bother to pass an object of ErrNotifier, but I choose not to do so because method signatures with a parameter of type ErrNotifier helps you spot that the methods require error handling.

Usage #

Initialisation and clean-up #

Instantiate ErrFlow, with the default value representing that there is no error. The value is used as the initial value in the notifier of each scope().

When the ErrFlow object is no longer needed, call dispose() to ensure that the resources held in the object will not be used any more.

enum ErrorTypes {
  none,
  foo,
  bar,
}

...

final errFlow = ErrFlow<ErrorTypes>(ErrorTypes.none);

...

errFlow.dispose();

Setting/logging an error #

  1. Call set() on an ErrNotifier object when some exception happens.
    • The object is passed from scope(), which is described later in this document.
  2. The listener is notified of the error and stores it as the last error (lastError) so that it can be checked later inside the function executed by scope().
  3. The listener also calls the logger to log a set of information about the exception if it is provided via set() or log().
Future<bool> yourMethod(ErrNotifier notifier) {
  try {
    return errorProneProcess();
  } catch(e, s) {
    // This updates the last error value and also triggers logging.
    notifier.set(ErrorTypes.foo, e, s, 'additional info');

    // Provide only the error value if logging is unnecessary.
    notifier.set(ErrorTypes.foo);

    // Use log() instead, if you consider the exception as
    // non-problematic and want to just log it.
    notifier.log(e, s, 'additional info');
  }

  // You can use hasError to check if some error was set.
  if (notifier.hasError) {
    ...
  }

  return false;
}

Handling errors #

scope() executes a function, and handles errors occurring inside there when the function finishes according to the conditions specified by errorIf and criticalIf. Use both or either of them to set the conditions of whether to treat the function result as a non-critical/critical error. The condition of criticalIf is evaluated prior to that of errorIf.

If either of the conditions is met, the relevant handler, onError or onCriticalError, is called. Do some error handling in these handlers, like showing different messages depending on the severity of the error.

final result = await errFlow.scope<bool>(
  (notifier) => yourMethod(notifier),
  errorIf: (result, error) => error == ErrorTypes.foo,
  criticalIf: (result, error) => error == ErrorTypes.bar,
  onError: (result, error) => _onError(result, error),
  onCriticalError: (result, error) => _onCriticalError(result, error),
);

The handler functions receive the function result and the error value, which means you can combine them to customise the conditions for your preference.

e.g. To trigger the onError handler if any error was set:

errorIf: (result, error) => error != errFlow.defaultError

e.g. To trigger the onError handler when the process fails for reasons other than a connection error:

errorIf: (result, error) => !result && error != ErrorTypes.connection

Ignoring errors #

If a method, in which set() is called on an exception, is called from some different places in your code, you may want to show an error message at some of them but not at the others. In such a case, you can control whether to handle the error, only log it instead of handling it, or ignore it completely.

loggingScope()

notifier passed from loggingScope is an object of LoggingErrNotifier. Calls on that object to set() are forwarded to log(), meaning that the error handlers are not triggered.

await errFlow.loggingScope<bool>(
  (notifier) => yourMethod(notifier),
);

ignorableScope()

notifier passed from ignorableScope is an object of IgnorableErrNotifier. Calls on that object to set() and log() are ignored, meaning that both the error handlers and the logger are not triggered.

await errFlow.ignorableScope<bool>(
  (notifier) => yourMethod(notifier),
);

Default error handlers #

You may want to consistently use a particular handler for non-critical errors, and the same or another one for critical errors. In such a case, errorHandler and criticalErrorHandler will come in handy. You can specify in advance how errors should be handled, and omit onError and onCriticalError in scope().

void _errorHandler<T>(T result, ErrorTypes error) {
  switch (error) {
    case ErrorTypes.foo:
      // Handle the foo error (e.g. showing the error details)
      break;
    default:
      // Handle other errors
      break;
  }
}

...

errFlow
  ..errorHandler = _errorhandler
  ..criticalErrorHandler = _errorHandler;

final result = await errFlow.scope<bool>(
  (notifier) => yourMethod(notifier),
  errorIf: (result, error) => !result,
);

Logger #

To use the default logger, which simply prints information to the console, call useDefaultLogger() before the first logging.

errFlow.useDefaultLogger();

If it lacks functionality you need, set your own logger.

Future<void> _logger(Object? e, StackTrace? s, {Object? reason}) async {
  // Logging operations
}

...

errFlow.logger = _logger;

In flutter, the recordError() method of the firebase_crashlytics package can be assigned to the logger as is.

import 'package:firebase_crashlytics/firebase_crashlytics.dart';

...

errFlow.logger = FirebaseCrashlytics.recordError;

Make sure to set the default or a custom logger, otherwise an assertion error will occur in the debug mode.

Adding/removing a listener #

This is usually unnecessary, but you can add a custom listener for your special needs.

void _listener({ErrorTypes? error, Object? exception, StackTrace? stack, Object? context}) {
  // Some processing
}

...

errFlow.addListener(_listener);

...

errFlow.removeListener(_listener);
0
likes
0
pub points
18%
popularity

Publisher

verified publisherkaboc.cc

A package for making it somewhat easier to comprehend the flow of errors and handle them.

Repository (GitHub)
View/report issues

License

unknown (LICENSE)

Dependencies

meta

More

Packages that depend on errflow