fix: async cache storing exception fixed (#548)

Co-authored-by: Moritz <mosum@google.com>
diff --git a/pkgs/async/CHANGELOG.md b/pkgs/async/CHANGELOG.md
index a45dc81..d84fbfb 100644
--- a/pkgs/async/CHANGELOG.md
+++ b/pkgs/async/CHANGELOG.md
@@ -11,7 +11,9 @@
 - Fix `StreamGroup.broadcast().close()` to properly complete when all streams in
   the group close without being explicitly removed.
 - Run `dart format` with the new style.
-
+- Can decide `fetch` method of `AsyncCache` will store exception or not
+  by using `cacheErrors` property.
+- 
 ## 2.13.0
 
 - Fix type check and cast in SubscriptionStream's cancelOnError wrapper
diff --git a/pkgs/async/lib/src/async_cache.dart b/pkgs/async/lib/src/async_cache.dart
index f86b060..7ce667d 100644
--- a/pkgs/async/lib/src/async_cache.dart
+++ b/pkgs/async/lib/src/async_cache.dart
@@ -36,6 +36,15 @@
   /// Cached results of a previous [fetch] call.
   Future<T>? _cachedValueFuture;
 
+  /// Whether the cache will keep a future completed with an error.
+  ///
+  /// If `false`, a non-ephemeral cache will clear the cached future
+  /// immediately if the future completes with an error, as if the
+  /// caching was ephemeral.
+  /// _(Ephemeral caches always clear when the future completes,
+  /// so this flag has no effect on those.)_
+  final bool _cacheErrors;
+
   /// Fires when the cache should be considered stale.
   Timer? _stale;
 
@@ -44,14 +53,20 @@
   /// The [duration] starts counting after the Future returned by [fetch]
   /// completes, or after the Stream returned by `fetchStream` emits a done
   /// event.
-  AsyncCache(Duration duration) : _duration = duration;
+  /// If [cacheErrors] is `false` the cache will be invalidated if the [Future]
+  /// returned by the callback completes as an error.
+  AsyncCache(Duration duration, {bool cacheErrors = true})
+      : _duration = duration,
+        _cacheErrors = cacheErrors;
 
   /// Creates a cache that invalidates after an in-flight request is complete.
   ///
   /// An ephemeral cache guarantees that a callback function will only be
   /// executed at most once concurrently. This is useful for requests for which
   /// data is updated frequently but stale data is acceptable.
-  AsyncCache.ephemeral() : _duration = null;
+  AsyncCache.ephemeral()
+      : _duration = null,
+        _cacheErrors = true;
 
   /// Returns a cached value from a previous call to [fetch], or runs [callback]
   /// to compute a new one.
@@ -62,8 +77,18 @@
     if (_cachedStreamSplitter != null) {
       throw StateError('Previously used to cache via `fetchStream`');
     }
-    return _cachedValueFuture ??= callback()
-      ..whenComplete(_startStaleTimer).ignore();
+    if (_cacheErrors) {
+      return _cachedValueFuture ??= callback()
+        ..whenComplete(_startStaleTimer).ignore();
+    } else {
+      return _cachedValueFuture ??= callback().then((value) {
+        _startStaleTimer();
+        return value;
+      }, onError: (Object error, StackTrace stack) {
+        invalidate();
+        throw error;
+      });
+    }
   }
 
   /// Returns a cached stream from a previous call to [fetchStream], or runs
diff --git a/pkgs/async/test/async_cache_test.dart b/pkgs/async/test/async_cache_test.dart
index 47204e6..464c1e1 100644
--- a/pkgs/async/test/async_cache_test.dart
+++ b/pkgs/async/test/async_cache_test.dart
@@ -18,6 +18,26 @@
     cache = AsyncCache(const Duration(hours: 1));
   });
 
+  test('should not fetch when callback throws exception', () async {
+    cache = AsyncCache(const Duration(hours: 1), cacheErrors: false);
+    Future<String> asyncFunctionThatThrows() {
+      throw Exception();
+    }
+
+    var errorThrowingFuture = cache.fetch(asyncFunctionThatThrows);
+    await expectLater(errorThrowingFuture, throwsA(isException));
+
+    FakeAsync().run((fakeAsync) async {
+      var timesCalled = 0;
+      Future<String> call() async => 'Called ${++timesCalled}';
+
+      expect(await cache.fetch(call), 'Called 1');
+
+      fakeAsync.elapse(const Duration(hours: 1));
+      expect(await cache.fetch(call), 'Called 2');
+    });
+  });
+
   test('should fetch via a callback when no cache exists', () async {
     expect(await cache.fetch(() async => 'Expensive'), 'Expensive');
   });