Articles
File Upload Coding Interview: Progress, Cancellation, Retries, and Preview Cleanup
Practice a file upload coding interview with accurate progress, cancellation, safe retries, bounded concurrency, and object URL preview cleanup.
An upload component can display 100% and still fail. That percentage may mean the browser has sent bytes, while the server is still checking, storing, or rejecting them. In a file upload coding interview, build around that distinction: model each file and each attempt separately, show only trustworthy progress, cancel the active transport, retry within a clear server contract, and release preview resources when they are no longer used.
The state machine and examples below are an original exercise, not a claim about a specific employer's exact prompt.
Clarify the contract before choosing an API
Ask whether the browser uploads to the application server or directly to object storage using a scoped URL. Does the server provide a completion or verification endpoint? Is each file independent, or must a batch succeed atomically? Are uploads resumable, or does retry restart from byte zero? What are the size, type, and concurrency limits? Can users remove an already accepted file?
Those questions are not hypothetical polish. A direct upload can finish sending bytes while a separate backend acknowledgement is pending. A “retry” button cannot resume an interrupted multipart transfer unless the server exposes upload IDs and part-level state. If a candidate silently treats all retries as resumable or all progress as server acceptance, the UI will lie under failure.
For this exercise, assume independent files, at most two active requests, a conventional single-request upload per file, and a server response that returns an accepted file ID. Retries restart the file from zero. Files can share names, so the UI assigns a unique client ID at selection time. A server ID appears only after acceptance.
Separate file identity from attempt identity
Represent a selected file as {clientId, file, previewUrl, status, attemptId, sentBytes, totalBytes, serverId, error}. The File object and clientId persist across retries. The attemptId changes every time a new request starts. Event handlers capture the attempt ID and ignore events whose ID no longer matches the current file state.
Why bother? Suppose attempt 1 fails and the user starts attempt 2. A late loadend or timeout handler from attempt 1 should not replace attempt 2's “uploading” status with “failed.” The same pattern protects against a cancellation event arriving just as the server responds. Stable file identity handles rendering and removal; attempt identity handles racing network events.
Do not key React rows by filename or array index. Two resume.pdf files are distinct selections. Removing the first should not transfer its progress and abort controller to the second. The file's clientId should also be the key in maps of active requests and preview URLs.
Use a state machine with explicit terminal states
The following state model keeps a user-visible percentage separate from acceptance. queued means waiting for a concurrency slot; uploading means the transport has begun; verifying means the bytes have been sent but the request or server acknowledgement is not yet successful. complete, failed, and cancelled are terminal for one attempt, not necessarily for the file forever.
| Current state | Event | Next state | Essential guard |
|---|---|---|---|
| selected | validation passes | queued | File still exists in list |
| queued | slot opens | uploading | Create a new attempt ID |
| uploading | upload progress | uploading | Event belongs to current attempt |
| uploading | bytes sent | verifying | Do not call it complete yet |
| uploading/verifying | server accepts | complete | Response belongs to current attempt |
| queued/uploading/verifying | user cancels | cancelled | Remove queue entry or abort active request |
| uploading/verifying | transport/server error | failed | Preserve safe error category |
| failed/cancelled | user retries | queued | Reset progress and create a new attempt |
An HTTP error is not a successful upload simply because the browser sent every byte. The result must satisfy the server's acceptance contract. Preserve a distinction between validation errors (not retryable without changing the file), transient network errors, authorization failures requiring a new session or URL, and server processing failures whose retry semantics need clarification. Do not indiscriminately retry every status.
Choose upload progress deliberately
For widely compatible browser upload progress, use XMLHttpRequest.upload events and check event.lengthComputable before calculating a percentage. MDN's upload-event reference lists progress, load, error, abort, and loadend. loaded / total describes upload transfer progress when the length is known; it is not proof that application-side processing committed. MDN's XHR guide also distinguishes upload and download events.
The MDN file-upload example uses XHR because its Fetch-based path does not expose equivalent upload progress in that example. Do not claim Fetch can never upload files; it can. The choice here is about the progress signal required by this exercise and browser compatibility, not about Fetch's ability to send a body.
If total length is unknown, show an indeterminate spinner with the current state text instead of a fabricated percentage. Cap a transfer-stage visual at “sending complete” until the server responds; then move to “processing” or “complete.” If the server has a long-running scan, its separate status endpoint may expose different progress. Do not splice those percentages into one fake linear number without explaining their meaning.
Cancellation is a request, not proof of rollback
For an active XHR, call .abort() and update the UI for the current attempt. For a queued file, remove it from the queue without opening a request. For a completed file, “Remove” is a separate server operation if the product needs deletion. Never label that action “Cancel upload” after acceptance.
At the server boundary, abort does not guarantee that zero bytes arrived or that no object was created. The server may have accepted the request just before the client disconnected. A robust API may use an idempotency key, upload session, or status lookup to reconcile an ambiguous result. State this uncertainty in an interview; the browser alone cannot prove server rollback.
Also guard against completion racing cancellation. If the user clicks Cancel at the same moment a success response arrives, define a single transition order. One reasonable policy is: if the server has already returned an accepted ID, show complete and offer Remove; otherwise mark cancelled and reconcile later if the backend supports lookup. The important part is that the UI does not simultaneously show cancelled and complete for the same attempt.
Retry without duplicating effects
When a retry begins, reset sentBytes, clear the prior transport error, and assign a new attempt ID. Reuse the selected File; do not require the user to browse again unless the browser session no longer holds it. If the request needs a short-lived upload URL, obtain a fresh one instead of assuming the old signed URL survives a failed attempt.
Retry only a failure the server contract identifies as safe. Network timeout after the final byte is ambiguous: the server might have accepted the file but the response was lost. If the API supports an idempotency key or client upload ID, reuse that logical key to discover or safely complete the same upload. Without that contract, say the UI cannot guarantee duplicate-free retries and propose a status check. Exponential backoff and jitter are useful for bounded automatic retries, but a manual Retry action should still report what happened and stop after a sensible limit.
Do not leave an old timer pending after cancellation. A scheduled retry that wakes after the user removed the file is a common bug. Tie timers, controllers, and attempt IDs to a cleanup path; both abort and clearTimeout belong there.
Bound concurrency and preserve fairness
With a limit of two, select files A, B, and C. Start A and B; C is queued. If A fails and the user retries it immediately, decide whether C or the retry gets the next slot. FIFO queueing gives C the slot and returns A to the back. That choice avoids one bad file starving the rest. If the user cancels queued C, no request should ever begin for it.
The scheduler's invariant is activeCount <= 2, including requests that have not yet delivered a terminal event. Release a slot exactly once. loadend may arrive after error or abort; multiple handlers should converge on one guarded finalize operation rather than decrementing twice. A file map and FIFO queue are usually enough for the interview; a global retry framework is not.
Use a three-file trace to demonstrate it: at t0 A and B upload while C waits; at t1 A fails, so C starts; at t2 the user retries A, which queues; at t3 B completes, so A starts a new attempt; at t4 an old A error event arrives and is ignored by the attempt guard. If your design produces three active requests or a failed label on the retried A, revise it.
Make preview cleanup part of correctness
For local image previews, URL.createObjectURL(file) avoids loading a full base64 string into UI state. Each created object URL must eventually be passed to URL.revokeObjectURL. MDN's File API example notes that each call creates a distinct URL and warns against revoking it immediately after image load if the user still needs to interact with the image.
Create one URL per selected file, reuse it across upload retries, and revoke it when the row is removed, replaced, or the component unmounts. Do not revoke simply because upload reached 100%; the preview may still be visible. If a file is replaced under the same UI row, revoke the old URL before adopting the new one. For non-previewable files, render name, type, and size without inventing a thumbnail.
Client validation can reject obviously oversized or unsupported files early, but the server must still validate bytes, authorization, type, and storage policy. A browser-provided MIME type and filename are hints, not a security boundary. Keep error text safe: do not echo raw server internals or secret-bearing signed URLs into the UI.
Test events in hostile order
Unit-test the reducer or state transition function with explicit attempt IDs. Simulate progress after cancellation, old completion after retry, two terminal events for one XHR, and duplicate filenames. Verify that an indeterminate transfer never displays a numerical percentage and that a transport reaching 100% remains verifying until the acceptance response.
In a rendered test, select three files, verify only two start, cancel a queued item, fail one active request, and retry it. Spy on .abort() for active cancellation and URL.revokeObjectURL when a preview is removed or the component unmounts. Use a server stub that returns a lost-response scenario to exercise the ambiguous-success copy. Accessibility tests should find a text status and an action button for each file by its stable label; progress should not be color-only.
Give the interviewer a precise closing answer
“Each selected file has a stable client ID, and each network try has a separate attempt ID. I use an explicit queue and a two-request limit. XHR upload progress reports bytes sent only; a server acceptance response moves the file to complete. Cancel aborts the active transport but does not claim server rollback. Retry creates a new attempt and reconciles ambiguous success through the backend contract. Preview object URLs live as long as the row needs them and are revoked on removal or unmount.”
That is a complete file upload coding interview answer because it names the uncertainty at each boundary. Practice the bounded-concurrency question and then add the per-file state machine as your own extension. If the prompt gives a different backend contract, change only the transitions it actually affects.
Sources and Further Reading
- MDN: Using files from web applications
- MDN: XMLHttpRequest upload events
- MDN: Using XMLHttpRequest
- MDN: URL.revokeObjectURL
The upload scheduler and state transitions are original practice examples.