You create a repository method to load a user's name. Sometimes the value is already in memory. Sometimes it must be fetched asynchronously. Should the method always return Future<String>, or should it return FutureOr<String>?
That decision affects your API contract, its callers, error handling and how you connect it to Flutter widgets. This guide explains Future vs FutureOr in Flutter with practical Dart examples and the trade-offs behind each choice.
Future<T> gives the caller a future. FutureOr<T> permits either a value of type T or a Future<T>. Choose a future for a consistently asynchronous interface; use the broader type when accepting both forms serves a clear purpose.What Is Future<T> in Dart?
A Future<T> represents the result of an asynchronous computation. It can complete with a value of type T or an error. A future object is distinct from the value it eventually supplies.
A method returning Future<String> can use async and return a string inside its body:
Future<String> loadName() async {
// Simulates an asynchronous data source.
await Future<void>.delayed(
const Duration(milliseconds: 200),
);
return 'Amit';
}
The caller still receives a future, not a plain string. Use await to obtain the result and try/catch to handle an awaited failure. Reference: Dart Future API.
What Is FutureOr<T>?
FutureOr<T>, imported from dart:async, describes a future-or-value type. For example, a FutureOr<int> can hold an integer or a future that completes with an integer.
import 'dart:async';
FutureOr<int> getCount(bool cached) {
if (cached) {
return 12;
}
return Future<int>.value(12);
}
It is a type declaration, not a wrapper you construct around a result. You cannot instantiate, extend or implement FutureOr. The method above remains non-async, allowing its cache branch to return a direct value. Reference: Dart FutureOr API.
Future vs FutureOr: The Main Differences
| Aspect | Future<T> | FutureOr<T> |
|---|---|---|
| Declared result | A future | A value or a future |
| Typical caller | Uses await or a future callback | Uses await, or deliberately handles both forms |
| Practical fit | A stable async service interface | A mixed sync/async callback or cache-aware contract |
| Calling .then() | Available on the future | Requires narrowing or conversion first |
| FutureBuilder input | Can supply its future | Convert to a future when necessary |
Neither option is universally better. A return type is a promise to callers about the kinds of result they must handle. Prefer the contract that makes that responsibility clear.
A Cache-First Repository Using FutureOr
This standalone Dart example returns a cached name directly and uses a future on the first lookup. The delay simulates asynchronous work; there is no backend or Flutter dependency.
import 'dart:async';
class NameRepository {
String? _cachedName;
FutureOr<String> getName() {
final cached = _cachedName;
if (cached != null) {
return cached;
}
return _fetchAndCache();
}
Future<String> _fetchAndCache() async {
await Future<void>.delayed(
const Duration(milliseconds: 200),
);
const name = 'Amit';
_cachedName = name;
return name;
}
}
Future<void> main() async {
final repository = NameRepository();
final first = repository.getName();
print(first is Future<String>); // true
print(await first); // Amit
final second = repository.getName();
print(second is String); // true
print(await second); // Amit
}
What this example deliberately leaves out
A real repository needs decisions about freshness, invalidation and duplicate requests. Two callers reaching the empty cache together can each trigger a fetch in this simple implementation. The return type does not provide request deduplication or a cache policy.
Before adopting it, decide what happens when the user signs out, the underlying data changes or the network request fails. For user-specific information, make sure the cache lifetime matches the account lifetime.
Can You await FutureOr?
Yes. Dart permits awaiting an expression that produces either a future or an ordinary value. This lets a caller consume both branches through one path:
Future<void> showName(NameRepository repository) async {
try {
final name = await repository.getName();
print(name);
} catch (error) {
print('Could not load the name: $error');
}
}
The try block includes the method call itself, so it covers a synchronous exception during that call as well as an error from the awaited future. Reference: Dart async and await.
However, consuming the result with await does not preserve a purely synchronous continuation on a cache hit. If immediate synchronous consumption is the reason for your contract, account for that explicitly. For many app screens, a single consistent awaited path is easier to maintain.
Converting FutureOr to Future: Future.value vs Future.sync
Future.value: Convert a result you already have
Future<T>.value accepts a value or future. If given a future, the returned future follows its completion, including an error.
final FutureOr<String> result = repository.getName();
final Future<String> future = Future<String>.value(result);
This conversion happens after the repository call. It does not retroactively capture a synchronous exception thrown by that call. Reference: Future.value.
Future.sync: Invoke a computation and capture its result
Future<T>.sync accepts a callback. It invokes that callback immediately, handles its value or future result, and turns a thrown exception into an error on the returned future.
Future<String> loadNameAsFuture(NameRepository repository) {
return Future<String>.sync(repository.getName);
}
The argument here is a method reference, not the result of calling it. This is an intentional boundary where callers receive one consistent future-based interface. Reference: Future.sync.
Future.sync does not schedule the computation on a background thread. Its callback runs immediately. Wrapping expensive synchronous work in it does not make that work stop blocking the current isolate.Why FutureOr Is Useful for Callback APIs
Suppose a validator may use a local rule or an asynchronous service. Letting the callback return either form can keep the caller's implementation simple.
import 'dart:async';
typedef Validator = FutureOr<bool> Function(String value);
Future<bool> validate(String value, Validator validator) async {
return await validator(value);
}
Future<void> main() async {
final localResult = await validate(
'Amit',
(value) => value.trim().isNotEmpty,
);
final asyncResult = await validate(
'Amit',
(value) async {
await Future<void>.delayed(
const Duration(milliseconds: 100),
);
return value.length >= 3;
},
);
print(localResult); // true
print(asyncResult); // true
}
This is a standalone second example. Dart's own Future.then() uses a future-or-value callback result: the callback can provide a transformed value or return another future, and the chain follows that result. Reference: Future.then().
For a reusable callback API, document when the callback is invoked, what failures mean and whether cancellation or repeated calls are possible. The result type alone does not answer those questions.
Using FutureOr with FutureBuilder in Flutter
FutureBuilder<T> expects a Future<T>?, so a mixed return needs normalisation. Obtain and store the future outside build() to avoid restarting work on rebuilds. Even an already-completed future can produce a waiting frame. Reference: FutureBuilder.
Use this widget alongside the NameRepository class above. Place it beneath your app's directionality and default text styling. Here the repository reference remains stable for the lifetime of the widget.
import 'dart:async';
import 'package:flutter/widgets.dart';
class NamePanel extends StatefulWidget {
const NamePanel({super.key, required this.repository});
final NameRepository repository;
@override
State<NamePanel> createState() => _NamePanelState();
}
class _NamePanelState extends State<NamePanel> {
late final Future<String> _nameFuture;
@override
void initState() {
super.initState();
_nameFuture = Future<String>.sync(widget.repository.getName);
}
@override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: _nameFuture,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Text('Loading name...');
}
if (snapshot.hasError) {
return const Text('Unable to load the name.');
}
return Text(snapshot.data ?? 'No name available');
},
);
}
}
For a refresh button or a changing repository, make the stored future replaceable and update it deliberately. If the parent supplies different request inputs, use the appropriate lifecycle handling rather than leaving an old future attached to new inputs.
When Should You Choose Future or FutureOr?
Choose Future for a consistent service boundary
My default recommendation for an application repository is a consistent asynchronous interface when its consumers already await every call. This avoids exposing cache state through the result's shape and reduces the number of branches callers need to understand.
A cache hit can still be efficient behind that interface. Returning a future does not imply every call performs a network request, and caching does not require a future-or-value return type.
Choose FutureOr when both forms are part of the design
Use it when a callback should accept local and asynchronous implementations, or when you deliberately need a direct-value path. Write down why that flexibility matters. If you cannot identify a caller that benefits, the broader contract may add complexity without a useful payoff.
Measure before claiming a performance win
Avoid promising that changing the annotation makes an app faster. Measure the real bottleneck: repeated requests, unnecessary parsing, rebuilding or expensive computation. For CPU-heavy work, consider suitable isolate-based tools with platform constraints; neither async nor FutureOr automatically moves work to another isolate. Reference: Dart isolates.
Common Future and FutureOr Mistakes
Calling .then() directly on a mixed result
The result might be a plain value. Use await or convert it before calling future methods.
Adding async while expecting a direct cached return
An async function produces a future even when its body returns a cached value. For an API that always behaves asynchronously, declare Future<T> clearly.
Ignoring two different failure paths
A non-async mixed-result producer can throw during invocation or return a future that later fails. Put the invocation inside your try block or use a deliberate normalisation boundary.
Using dynamic to avoid deciding on a contract
Keep the expected value type explicit. If the result is a name, prefer a string-based contract over an untyped result that pushes mistakes into runtime.
Using FutureOr<void> for a completion contract
Dart's avoid_futureor_void lint warns against this return type because it is difficult to use correctly. Prefer Future<void> when callers must wait for completion, or void for a deliberately synchronous interface. Reference: Dart lint guidance.
Frequently Asked Questions
Is FutureOr a Flutter-specific feature?
No. It belongs to Dart's asynchronous library and can be used outside Flutter.
Is FutureOr faster than Future?
It permits a direct-value result, but does not guarantee a faster application. Benchmark your actual use case.
Can FutureOr be nullable?
Use a nullable value type when null is a valid result—for example, FutureOr<String?> for an immediate nullable string or a future yielding one.
Can I pass FutureOr directly to FutureBuilder?
Normalise it to the future type expected by the widget, and retain that future across ordinary rebuilds.
Should every cached method return FutureOr?
No. Choose the public contract based on caller needs, independently of your internal caching strategy.
Does Future.sync move work into the background?
No. It invokes its computation immediately and normalises its result and errors.

