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; implements takes the API without the implementation, extends takes both, with layers in mixins.
  • There is no private keyword: an underscore prefix makes a member library-private.
  • Getters and setters are declared with get and set and are used like fields by callers.
  • @override is not required but the analyser warns on a missing annotation, which catches silently renamed base methods.
  • Fields declared final are 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();
}
AspectFutureStream
Values deliveredExactly one, or an errorZero or many, then done or an error
Await it withawaitawait for
Declared withasync and returnasync* and yield
CancellableNoYes, through the subscription
Typical sourceOne HTTP requestFile bytes, websockets, UI events
ReusableA Future is single-useA 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.

Dart syntax and null safety Isolates, compilation and packaging

Last refreshed 2026-09-18.