Is there a way to throw generics in dart

Issue

I’m trying to make a generic function to handle exceptions to reduce code duplications.

the function is like this:

Future<T> exceptionHandler<T, E extends Exception>({
  required ValueGetter<Future<T>> tryBlock,
}) async {
  try {
    return await tryBlock();
  } catch (e) {
    if (e is E) {
      rethrow;
    }

    throw E( // Error: E isn't a function
      error: e.toString(),
    );
  }
}

I have created custom exceptions, one of them is called
AuthException.

This function is intended to be used as follows:

Instead of doing this:

Future<bool> signUp({
    required covariant UserModel user,
    required String password,
  }) async {
    try {
      // try block code goes here
    } catch (e) {
      if (e is AuthException) {
        rethrow;
      }

      throw AuthException(
        error: e.toString(),
      );
    }
  }

I want to be able to do this:

Future<bool> signUp({
    required covariant UserModel user,
    required String password,
  }) async {
    return await exceptionHandler<bool, AuthException>(
      tryBlock: () async {
        // try block goes here
      },
    );
  }

The AuthException is defined like this:

class AuthException implements Exception {
  final String error;

  const AuthException({
    required this.error,
  });
}

The problem is that I can’t throw E from the exceptionHandler function because E is a type and not an object. Is there a way to do this?

Solution

You cannot directly construct an instance of a generic type argument. Dart does not consider constructors and static methods to be part of the class’s interface, so the restriction of E extends Exception does not guarantee that E has an unnamed constructor that takes a named argument error.

You instead can have your generic function take a callback that constructs the type you want:

Future<T> exceptionHandler<T, E extends Exception>({
  required ValueGetter<Future<T>> tryBlock,
  required E Function({String error}) makeException,
}) async {
  try {
    return await tryBlock();
  } catch (e) {
    if (e is E) {
      rethrow;
    }

    throw makeException(error: e.toString());
  }
}

and then usage would be:

Future<bool> signUp({
  required covariant UserModel user,
  required String password,
}) async {
  return await exceptionHandler(
    tryBlock: () async {
      // try block goes here
    },
    makeException: AuthException.new,
  );
}

I’ll also note that your catch block will catch everything, including Errors, which is usually frowned upon, and it loses the stack trace, which will make failures harder to debug. I’d restrict the catch clause to Exceptions and use Error.throwWithStackTrace:

Future<T> exceptionHandler<T, E extends Exception>({
  required ValueGetter<Future<T>> tryBlock,
  required E Function({String error}) makeException,
}) async {
  try {
    return await tryBlock();
  } on Exception catch (e, stackTrace) {
    if (e is E) {
      rethrow;
    }

    Error.throwWithStackTrace(
      makeException(error: e.toString(),
      stackTrace,
    );
  }
}

Answered By – jamesdlin

Answer Checked By – David Marino (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *