Master Future, async/await, and FutureBuilder without blocking the UI or accidentally re-firing your requests.
Open this lesson in KodokonDart runs your application on a single thread, driven by an event loop. A Future is not a thread: it's the promise of a value to come. await never blocks that thread - it suspends the current function, hands control back to the loop (the UI keeps running), then resumes when the result arrives. Corollary: async/await parallelizes nothing for pure computation; for CPU-intensive work, you need an isolate.
Future<double> fetchRate() async {
await Future<void>.delayed(
const Duration(milliseconds: 300),
);
return 1.08;
}
Future<void> main() async {
try {
final rate = await fetchRate();
print('EUR/USD rate: $rate');
} on Exception catch (error) {
print('Rate loading failed: $error');
}
}Two successive awaits run serially: the second only starts after the first. When the operations are independent, launch them together with Future.wait. The trade-off to know: Future.wait is fail-fast - the first error rejects the whole thing and the results of the other futures are lost, even if they succeed. If you need partial results, handle the error inside each future.
Future<String> fetchProfile() async => 'profile';
Future<String> fetchOrders() async => 'orders';
Future<void> main() async {
final [profile, orders] = await Future.wait(
[fetchProfile(), fetchOrders()],
);
print('$profile / $orders');
}On the UI side, FutureBuilder connects a Future to build via a snapshot. The number-one pitfall in the field: creating the future directly in build. Every rebuild - a simple setState from a parent is enough - would then re-fire the request. Store the future in initState and always test for error before data.
import 'package:flutter/material.dart';
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override
State<ProfileScreen> createState() =>
_ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
late final Future<String> _nameFuture;
@override
void initState() {
super.initState();
_nameFuture = _loadName();
}
Future<String> _loadName() async {
await Future<void>.delayed(
const Duration(seconds: 1),
);
return 'Ada Lovelace';
}
@override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: _nameFuture,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
return Text(snapshot.data!);
},
);
}
}await actually do on the main thread?Future resolves.Future.wait([a(), b()]), what happens if a() fails?Future.wait rejects with the error from a(); the result of b() is lost.Future.wait waits for b() to finish, then returns its result alone.b() succeeds.future: fetchUser() written directly in build with FutureBuilder?build can't contain an asynchronous call, the code doesn't compile.Future, and therefore re-fire the network request.snapshot would stay stuck in a waiting state.