Handling error and loading states
Every async screen has at least four states, and the third and fourth are the ones most often forgotten:
- Loading — data is on the way
- Data — there is a result
- Empty — the request succeeded but returned nothing (this is NOT an error)
- Error — the request failed
Rendering an empty list as simply nothing is the classic UX mistake: users assume the app is broken. An empty state should explain — why it is empty and what they can do.
There are two ways to surface errors to the UI:
- Throwing exceptions — Dart's native mechanism, caught with
try/catch. Simple, but the signature does not reveal that failure is possible - Returning a Result/Either type —
sealed class Result { Success | Failure }; the possibility of failure is visible in the type andswitchforces you to handle it
Both are legitimate. What matters is consistency: mixing the two in one codebase is the worst option.
An equally important rule: never show a raw technical error to the user. SocketException: Failed host lookup means nothing to them — map errors to domain types in the data layer and let the UI show a human message.
Interview tip. Saying try/catch is not enough for "how do you handle errors?". A strong answer has three elements: (1) where the error is caught — in the data layer, mapped to a domain type; (2) what the user sees — a human message and a retry action; (3) what the developer sees — logging or crash reporting. Mention separating the empty state from the error state too — few candidates do.
📚 Sources and documentation
- Networking and dataofficialdocs.flutter.dev
- Architecture guideofficialdocs.flutter.dev
- FutureBuilder classofficialapi.flutter.dev
Shows how to handle each state via `snapshot.hasError` and `connectionState`.