Every async request passes through explicit states: loading → success(data) or error(message). This is a discriminated union — a typed state machine, not three separate booleans.
The problem it solves
Separate isLoading, data, error fields are easy to get out of sync. After a failure someone forgot to reset data, and the UI shows old data with an error banner on top. Impossible flag combinations spread like weeds.
Operators and why they matter
startWith({kind: 'loading'}) — emit the loading state immediately, BEFORE the response.
map(data => ({kind: 'success', data})) — a successful response becomes a success state.
catchError(error => of({kind: 'error', error})) — a failure becomes an error state, WITHOUT killing the outer stream.
Gotchas
tap with manual this.isLoading=true/false flags — harder to test than state-as-data.
catchError outside the trigger stream — one error stops all future reloads. Put it INSIDE switchMap.
Don't mix idle and loading. idle = "no request yet". loading = "a request is in flight". Different states.
What you get
The UI gets one typed state and renders it with a switch. No more impossible combinations.