deadline_future 🏁

pub.flutter-io.cn Dart SDK License: BSD-3 style: lints

A time-bounded Future that never throws TimeoutException.
Returns the freshest data available β€” live, cached, or a static fallback β€” instead of crashing.


The Problem

// ❌ Dart's built-in Future.timeout β€” "all or nothing"
try {
  final price = await fetchPrice().timeout(const Duration(seconds: 2));
} on TimeoutException {
  // The result was discarded even if it arrived 1ms later.
  // You must handle the exception every single time.
}

In real-time apps this is painful: every slow network spike crashes the UI, every late response is wasted, and you have no visibility into why the fallback was used.


The Solution

// βœ… deadline_future β€” three-tier graceful fallback
final result = await fetchPrice().withDeadline(
  const Duration(seconds: 2),
  fallback: lastKnownPrice,      // πŸ›‘οΈ tier 3: static safety net
  cacheKey: 'btc_price',         // πŸ’Ύ tier 2: automatic smart cache
  cacheTtl: const Duration(minutes: 5),
  onTimeout: () => showSpinner(), // called the moment deadline hits
  context: 'BTC price widget',   // appears in logs & exceptions
);

// result is ALWAYS available β€” never null, never an exception
switch (result.source) {
  case DeadlineResultSource.completed:
    print('βœ… Live  β€” ${result.actualDuration!.inMilliseconds}ms');
  case DeadlineResultSource.cached:
    print('πŸ’Ύ Cached β€” showing last known value');
  case DeadlineResultSource.fallback:
    print('πŸ›‘οΈ  Fallback β€” network is struggling');
}

if (result.isDegraded) showStaleBadge(); // one-liner UI indicator
updatePrice(result.value);               // always works

Resolution Strategy

withDeadline(deadline, fallback: F, cacheKey: K)
          β”‚
          β”œβ”€ Future completes in time?      β†’ βœ… live result
          β”‚                                    (stored in cache for next time)
          β”‚
          β”œβ”€ Timeout + cache[K] valid?      β†’ πŸ’Ύ cached result
          β”‚
          β”œβ”€ Timeout + F != null?           β†’ πŸ›‘οΈ  fallback result
          β”‚
          └─ Timeout + nothing available?   β†’ πŸ”΄ DeadlineExceededException

Self-healing cache: even after a timeout, the original Future keeps running. When it finally completes, its value is stored in the cache β€” automatically improving the next call.


Installation

dependencies:
  deadline_future: ^0.1.0
dart pub get

Quick-start Recipes

Minimal β€” static fallback only

final result = await fetchUserProfile().withDeadline(
  const Duration(seconds: 2),
  fallback: UserProfile.guest(),
);
print(result.value.displayName); // always available

Smart cache β€” best for repeated calls

// First call: Future wins β†’ cached.
await fetchBtcPrice().withDeadline(
  const Duration(seconds: 2),
  cacheKey: 'btc',
  cacheTtl: const Duration(minutes: 5),
);

// Second call: network degraded β†’ served from cache.
final r = await fetchBtcPrice().withDeadline(
  const Duration(milliseconds: 300),
  cacheKey: 'btc',
  fallback: 0.0,
);

Duration shorthand

// Clean, readable deadlines:
await fetch().withDeadline(3.seconds);
await fetch().withDeadline(500.milliseconds);
await fetch().withDeadline(2.minutes);

Batch concurrent calls

final results = await [fetchBtc(), fetchEth(), fetchSol()]
    .withDeadlineAll(
      const Duration(milliseconds: 500),
      cacheKeys: ['btc', 'eth', 'sol'],
      fallback: 0.0,
      onTimeout: (i) => print('Feed $i timed out'),
    );

Exception handling

try {
  await myFuture.withDeadline(const Duration(seconds: 1));
} on DeadlineExceededException catch (e) {
  // Only thrown when NO cache entry AND NO fallback exist.
  print('Exceeded ${e.deadline.inMilliseconds}ms β€” ${e.context}');
} on InvalidDeadlineDurationException {
  // Synchronous guard against Duration.zero / negative values.
}

Global configuration (app startup)

void main() {
  DeadlineConfig.enableGlobalCache = true;
  DeadlineConfig.defaultCacheTtl   = const Duration(minutes: 10);
  DeadlineConfig.maxCacheEntries   = 500;
  DeadlineConfig.logLevel          = kDebugMode
      ? DeadlineLogLevel.info
      : DeadlineLogLevel.silent;
  runApp(const MyApp());
}

API Reference

Future<T>.withDeadline()

Parameter Type Required Description
deadline Duration βœ… Max wait time. Must be positive.
fallback T? Static value returned on timeout (if cache miss).
cacheKey String? Enables smart cache. Unique per call site.
cacheTtl Duration? Per-call TTL. Overrides defaultCacheTtl.
onTimeout void Function()? Called the instant the deadline elapses.
context String? Label for logs and exception messages.

Returns: Future<DeadlineResult<T>>


DeadlineResult<T>

Member Type Description
value T The resolved value.
isTimedOut bool Did the deadline elapse?
source DeadlineResultSource completed, cached, or fallback.
isLive bool Shorthand: source == completed.
isDegraded bool Shorthand: !isLive.
isFromCache bool Shorthand: source == cached.
isFromFallback bool Shorthand: source == fallback.
actualDuration Duration? How long the original Future took.
resolvedAt DateTime UTC timestamp of resolution.
copyWith(...) DeadlineResult<T> Non-destructive field override.

DeadlineConfig (static)

Property / Method Default Description
enableGlobalCache true Master cache toggle.
defaultCacheTtl null Default TTL for all cache entries.
maxCacheEntries 200 Cache capacity before FIFO eviction.
ignoreErrorsAfterDeadline true Swallow late Future errors.
logLevel silent Controls stdout diagnostic output.
reset() β€” Restores defaults + clears cache.
clearCache() β€” Empties the cache only.
evictCacheEntry(key) β€” Removes one entry by key.
cacheSize β€” Current number of live cache entries.

Comparison Table

Feature Future.timeout() withDeadline()
Future completes in time βœ… Value βœ… Value + metadata
Timeout with handler βœ… onTimeout value βœ… Fallback / cache
Timeout without handler ❌ TimeoutException πŸ”Ά DeadlineExceededException*
Late result πŸ—‘οΈ Discarded πŸ’Ύ Cached for next call
Next call after timeout ❌ Crashes again βœ… Served from cache
Result metadata ❌ None βœ… DeadlineResultSource
onTimeout callback ❌ βœ…
Global config ❌ βœ… DeadlineConfig
Batch API ❌ βœ… withDeadlineAll
Duration shorthand ❌ βœ… 3.seconds

* Only thrown as a last resort β€” cache and fallback are checked first.


Ideal Use Cases

  • πŸ“ˆ Crypto / stock price feeds β€” show last known price while refreshing
  • πŸ’¬ Chat heads β€” display cached messages while server is slow
  • πŸ“Š Live dashboards β€” partial data is better than blank panels
  • 🏟️ Sports scores β€” stale score with "updating..." badge
  • πŸ”„ Retry wrappers β€” compose with withDeadline for per-attempt limits
  • 🌐 Any API call where "stale but available" beats "fresh but crashed"

Testing

dart test

Run the examples:

dart run example/main.dart

Run the benchmarks:

dart run benchmark/throughput_bench.dart

License

BSD-3-Clause Β© 2026 deadline_future contributors

Libraries

deadline_future
deadline_future β€” Graceful deadline handling for Dart Futures.