Classes, futures and streams
Constructors, inheritance and mixins, then async/await, error handling and the difference between a Future and a Stream.
Classes and interfaces
abstract class Shape {
double area(); // no body: subclasses must implement
}
mixin Named {
String describe() => 'shape';
}
class Circle with Named implements Shape {
final double radius;
Circle(this.radius); // parameter shorthand: assigns the field
Circle.unit() : radius = 1; // named constructor, initialiser list
@override
double area() => 3.14159 * radius * radius;
@override
String toString() => 'Circle($radius)';
}
class Square extends Shape {
final double side;
Square(this.side);
@override
double area() => side * side;
}
void main() {
final shapes = <Shape>[Circle(2), Square(3)];
for (final s in shapes) {
print('$s -> ' + s.area().toStringAsFixed(2)); // toString plus polymorphism
}
const c = Circle.unit();
print(c.describe());
// c.radius = 5; // compile-time error: the field is final
}- Every class implicitly defines an interface;
implementstakes the API without the implementation,extendstakes both,withlayers in mixins. - There is no
privatekeyword: an underscore prefix makes a member library-private. - Getters and setters are declared with
getandsetand are used like fields by callers. @overrideis not required but the analyser warns on a missing annotation, which catches silently renamed base methods.- Fields declared
finalare immutable, but that only freezes the reference: a final list can still be modified.
Futures and async/await
A Future is a placeholder for one value that arrives later. Marking a function async makes it return a Future immediately and lets you await inside it; the underlying thread is never blocked.
Future<String> fetchUser(int id) async {
await Future<void>.delayed(const Duration(milliseconds: 200));
if (id <= 0) throw ArgumentError('id must be positive');
return 'user-$id';
}
Future<void> main() async {
try {
final name = await fetchUser(1);
print(name);
} on ArgumentError catch (e) {
print('bad input: $e'); // specific type first
} catch (e, stack) {
print('unexpected: $e');
print(stack);
} finally {
print('done'); // runs on every path
}
// Sequential awaits add up; Future.wait runs them concurrently
final slow = await Future.wait([fetchUser(1), fetchUser(2)]);
print(slow.length);
// Mixing styles: then runs later on the same event loop
fetchUser(3).then((u) => print('then: $u')).catchError((Object e) {
print('error: $e');
return 'fallback';
});
// A Future has no synchronous value: this prints "instance of Future"
print(fetchUser(4));
}💡
async functions return their Future before the body finishes, and an exception thrown before the first await is still delivered through that Future. If a caller ignores the returned Future, the error surfaces as an unhandled asynchronous error instead of at the throw site.Streams
A Stream is a sequence of values over time. Use await for to consume one like a loop, or listen when you need pause, resume or cancel.
Stream<int> ticks(int n) async* {
for (var i = 1; i <= n; i++) {
await Future<void>.delayed(const Duration(milliseconds: 50));
yield i; // async* makes yield legal
}
}
Future<void> collect() async {
final seen = <int>[];
await for (final t in ticks(3)) {
seen.add(t);
}
print(seen); // [1, 2, 3]
}
void main() {
final sub = ticks(5).listen(
(value) => print('tick $value'),
onError: (Object e) => print('stream error: $e'),
onDone: () => print('finished'),
cancelOnError: false,
);
// Transformations are lazy; nothing runs until listen or await for
ticks(4)
.where((n) => n.isEven)
.map((n) => n * 10)
.listen(print);
sub.pause();
sub.resume();
sub.cancel();
}| Aspect | Future | Stream |
|---|---|---|
| Values delivered | Exactly one, or an error | Zero or many, then done or an error |
| Await it with | await | await for |
| Declared with | async and return | async* and yield |
| Cancellable | No | Yes, through the subscription |
| Typical source | One HTTP request | File bytes, websockets, UI events |
| Reusable | A Future is single-use | A broadcast stream can have many listeners |
FAQ
How do I run two requests at the same time?
Future.wait([a(), b()]) starts both and completes when both finish. Sequential await statements run one after the other and take the sum of their durations.Why does my try/catch not catch the error?
You probably called the async function without awaiting it, so the error travels through the discarded Future. Either await the call, or attach
.catchError. Unawaited futures are also flagged by the analyser in recent SDKs.Related
Dart syntax and null safety Isolates, compilation and packaging
Last refreshed 2026-09-18.