Lesson 10 of 30
A plain form-data POST works fine for a small file on a good connection — every real gap between that and a production upload experience comes from three specific, nameable problems.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A developer wants to show a real upload progress bar and reaches for XMLHttpRequest instead of fetch specifically to get this. Why?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A single fetch with FormData is enough to send a file, but it leaves three real gaps a production upload experience usually has to fill:
fetch call gives you no upload progress events at all. This is a genuine, still-relevant limitation, not something fixed by a newer API: showing a real progress bar requires either XMLHttpRequest's upload.onprogress event, or a library built on top of it, because fetch historically doesn't expose upload progress the way XMLHttpRequest does.Be honest about scope here: implementing chunking and resumability correctly — handling partial failures, verifying chunk integrity, resuming across page reloads — is real, nontrivial engineering, and most real products reach for an existing upload library or a managed upload service rather than hand-rolling the full protocol. The goal of this lesson is understanding why each piece matters and what problem it solves, not shipping a resumable-upload protocol from scratch.
XMLHttpRequest, chunking by slicing the filefunction uploadWithProgress(file: File, onProgress: (pct: number) => void) {
return new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append("file", file);
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
onProgress(Math.round((event.loaded / event.total) * 100));
}
};
xhr.onload = () => (xhr.status < 300 ? resolve() : reject(new Error(`Upload failed: ${xhr.status}`)));
xhr.onerror = () => reject(new Error("Network error during upload"));
xhr.open("POST", "/api/upload");
xhr.send(formData);
});
}
// chunking: split the file, upload each piece with its index so the server
// can reassemble it in order and retry just the pieces that fail
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
async function uploadInChunks(file: File, uploadId: string) {
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
for (let i = 0; i < totalChunks; i++) {
const start = i * CHUNK_SIZE;
const chunk = file.slice(start, start + CHUNK_SIZE);
await uploadChunkWithRetry(chunk, { uploadId, index: i, totalChunks });
// a real resumable implementation would persist "chunk i of uploadId
// succeeded" somewhere durable, so a retry after a page reload can
// skip straight to the first chunk that never got confirmed
}
}
A large file is split into four chunks before upload begins, instead of sent as one giant request.
fetch giving no upload progress is a real, still-current limitation, not an old problem someone already fixed. Reaching for XMLHttpRequest (or a library wrapping it) specifically for the progress event is a legitimate, current reason to use it over fetch."How would you handle uploading a large file with a progress bar that also survives a dropped connection?" is a real question that separates candidates who know fetch can't show upload progress and can name chunking/resumability as the actual fix for interruption, from those who only know the plain happy-path upload.