Sealed Classes for State

Medium PriorityAsked in ~55% of mid-level interviews (rising)

3 min read

State Management

Without sealed statesWith sealed states
if (state is Loading) chains, easy to miss a caseswitch over all cases, compiler-enforced
default: branches hide forgotten statesNo default needed — compiler checks
Adding a state silently breaks UIsAdding a state breaks the build
State has booleans like isLoading, isErrorEach state is its own type with the right fields

A sealed state class is the right tool for "this thing is in exactly one of N modes, each carrying different data."


Code in action

sealed class NetworkState<T> {}
class Initial<T>  extends NetworkState<T> {}
class Loading<T>  extends NetworkState<T> {}
class Success<T>  extends NetworkState<T> { final T data;        Success(this.data); }
class Failure<T>  extends NetworkState<T> { final String error;  Failure(this.error); }

// Exhaustive — no `default`, compiler proves all cases handled
Widget render(NetworkState<User> s) => switch (s) {
  Initial()                => const Text('Press load'),
  Loading()                => const CircularProgressIndicator(),
  Success(:final data)     => UserCard(data),
  Failure(:final error)    => ErrorBanner(error),
};

// Add a new state…
class Refreshing<T> extends NetworkState<T> { final T staleData; Refreshing(this.staleData); }

// Every `switch` over NetworkState fails to compile until you handle Refreshing.
// Refactoring becomes safe.

When sealed states pay off most

SituationWhy
Async data with init / loading / success / errorClassic ADT — sealed is perfect
Auth flow (out / loading / in / failed)Different payloads per state
Pagination (initial / loading next / has more / done)Easy to forget edge states without exhaustiveness
Multi-step forms / wizardsOne sealed step-state per phase
State machines in generalEvery transition is a new state subtype

Keep enums for labels without payloads (e.g. enum SortOrder { asc, desc }). Reach for sealed when each variant carries different data.


Common mistakes to avoid

// ❌ Adding `default:` and undoing exhaustiveness
switch (state) {
  case Loading(): return ...;
  default: throw 'unreachable';
}
// ✅ Drop the default — the compiler now tells you when you've missed a case

// ❌ Booleans masquerading as states
class UiState { bool isLoading; bool isError; User? data; String? error; }
// Mutually exclusive states modelled as parallel booleans → invalid combinations possible
// ✅ Use sealed states, one per mode

// ❌ Sealed without exhaustive switch — defeats the purpose
if (state is Success) ... else if (state is Failure) ... else ...
// ✅ switch (state) { Success() => ..., Failure() => ..., ... }

// ❌ Subclasses in different libraries
// lib/auth/state.dart:  sealed class AuthState {}
// lib/auth/extra.dart:  class ExtraState extends AuthState {}  ❌
// Sealed subtypes must live in the same library

// ❌ Sealing classes that don't need exhaustive handling
// → adds friction with no benefit. Seal when you actually switch on it.

Interview follow-ups

  1. Why does Dart require sealed subtypes to live in the same library? So the compiler can prove the set of subtypes is closed. If external packages could extend a sealed class, the compiler couldn't enumerate cases — the exhaustiveness guarantee would silently break downstream.

  2. Sealed class vs enhanced enum — when to use which? Enums for finite labels with no payload (Status.idle). Sealed classes when each case carries its own data (Loading() vs Success(user) vs Failure(error)). Dart 3 enhanced enums can hold fields, blurring the line, but the rule of thumb: payload variability → sealed.

  3. What changes about your BlocBuilder / ConsumerWidget code when you sealed your state? You replace if (state is X) chains with switch (state), drop the default branch, and rely on the compiler to flag missed cases. Pattern matching with :final data destructures fields cleanly inline.

  4. How do sealed states interact with == and hashCode? Same as any class — by default they use identity. For state classes you usually want value equality so BlocBuilder and Provider's select can de-dupe correctly. Pair sealed states with Equatable or freezed for automatic value equality.


How helpful was this content?

Please sign in to rate this article.