A boolean cannot explain a long-running job
Loading is often introduced as a boolean. That works until an operation can be queued, running, partially complete, cancelling, cancelled, or failed. When several booleans describe those conditions independently, impossible combinations become representable.
The following example is a conceptual model, not production code from an employer.
type JobState =
| { kind: 'idle' }
| { kind: 'running'; jobId: string }
| { kind: 'cancelling'; jobId: string }
| { kind: 'complete'; resultId: string }
| { kind: 'cancelled' }
| { kind: 'failed'; retryable: boolean };
Normalize at the service boundary
Different integrations may return different status names or incomplete payloads. A normalization layer should map supported responses to the model the UI understands. Unknown states deserve an explicit treatment; silently treating them as success is dangerous.
Normalization does not mean hiding uncertainty. If the service does not provide a completion estimate, the interface should not invent a precise percentage.
Cancellation is a request, not an outcome
A user pressing Cancel does not prove the job has stopped. The interface can acknowledge the request immediately while keeping a separate cancelling state until the service confirms a terminal result.
The same distinction applies to retry. A retry action may fail before a new operation starts. Clear transitions prevent a spinner from becoming the only record of what happened.
Test disagreement
Useful test cases include a delayed response from an earlier request, a permission change during work, a cancellation that races with completion, and a partial response missing expected metadata.
These cases force an engineering team to decide which source is authoritative and when the UI should show uncertainty. That decision is part of the product, not an afterthought for error handling.