Interview question
Build upload progress and retry recovery
Models file validation, progress, cancellation, failure, and retry without losing the selected file.
TL;DR
Models file validation, progress, cancellation, failure, and retry without losing the selected file.
File validation, upload lifecycle, progress announcements, cancellation, retry, and API errors.
Practice the problem like a real interview: restate, reason, implement, and test.
Accept one PDF under 10 MB. Keep the selected file after a recoverable failure. Announce coarse progress and allow cancel without calling it an error.
A failed upload at 60% shows Retry; retry uses the same file and starts a fresh request.
I model upload status explicitly instead of combining several booleans. Retry creates a new controller but retains the validated file.
type UploadState =
| { kind: "idle" }
| { kind: "ready"; file: File }
| { kind: "uploading"; file: File; progress: number }
| { kind: "failed"; file: File; message: string }
| { kind: "done"; fileId: string };
function selectFile(file: File) {
if (file.type !== "application/pdf" || file.size > 10_000_000) {
setValidationError("Choose a PDF under 10 MB.");
return;
}
setValidationError(null);
setState({ kind: "ready", file });
}
async function upload(file: File) {
const controller = new AbortController();
controllerRef.current = controller;
setState({ kind: "uploading", file, progress: 0 });
try {
const result = await uploadFile(file, p => {
const progress = Math.min(100, Math.floor(p / 10) * 10);
setState(current => current.kind === "uploading" && current.progress === progress
? current : { kind: "uploading", file, progress });
}, controller.signal);
setState({ kind: "done", fileId: result.fileId });
} catch {
if (controller.signal.aborted) setState({ kind: "ready", file });
else setState({ kind: "failed", file, message: "Upload failed. Retry when ready." });
}
}
// Cancel returns to ready; Retry calls upload(state.file) from failed state.
function cancelUpload() { controllerRef.current?.abort(); }
useEffect(() => () => controllerRef.current?.abort(), []);
A discriminated union prevents impossible combinations such as done and failed together. Real progress support depends on the upload transport.
Move to the linked follow-up, next path step, prerequisite, or deeper variant.
Practice the next layer of the same subject.