How to Track Upload Progress with JavaScript Fetch API
Problem
When I tried to add a progress bar for file uploads in my web application, I discovered that the fetch API doesn’t support upload progress tracking. I wanted something like this:
fetch('/upload', { method: 'POST', body: formData, onUploadProgress: (progress) => { console.log(`Uploaded: ${progress}%`); }});But fetch has no onUploadProgress option. I needed to find another way.
Environment
- JavaScript (ES6+)
- Modern browsers (Chrome, Firefox, Safari, Edge)
- No external dependencies preferred
Why Doesn’t Fetch Support This?
The fetch API was designed as a modern, Promise-based replacement for XMLHttpRequest. It provides a cleaner interface for most HTTP operations. But it omitted one critical feature: upload progress tracking.
This matters for:
- Large file uploads (videos, images, documents)
- User experience (showing progress bars)
- Mobile applications with unstable connections
- Timeout handling based on transfer speed
The irony is that XMLHttpRequest, which fetch was meant to replace, has had this feature for over 15 years.
Solution 1: XMLHttpRequest (No Dependencies)
I went back to XMLHttpRequest. It has the upload.onprogress event that gives real-time progress updates:
function uploadWithProgress(file, url, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { const percent = Math.round((event.loaded / event.total) * 100); onProgress(percent); } });
xhr.addEventListener('load', () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(xhr.response); } else { reject(new Error(`Upload failed: ${xhr.status}`)); } });
xhr.addEventListener('error', () => reject(new Error('Network error'))); xhr.addEventListener('abort', () => reject(new Error('Upload cancelled')));
xhr.open('POST', url);
const formData = new FormData(); formData.append('file', file); xhr.send(formData); });}
// UsageuploadWithProgress(file, '/upload', (percent) => { console.log(`Upload progress: ${percent}%`); updateProgressBar(percent);});The key parts:
xhr.upload.addEventListener('progress', ...)- captures upload progressevent.lengthComputable- checks if total size is knownevent.loaded / event.total- calculates percentage
Solution 2: Axios (Cleaner API)
If I don’t mind adding a dependency, axios wraps XMLHttpRequest and provides a cleaner Promise-based API:
import axios from 'axios';
async function uploadWithAxios(file, url, onProgress) { const formData = new FormData(); formData.append('file', file);
const response = await axios.post(url, formData, { onUploadProgress: (progressEvent) => { const percent = Math.round( (progressEvent.loaded * 100) / progressEvent.total ); onProgress(percent); }, });
return response.data;}
// Usageawait uploadWithAxios(file, '/upload', (percent) => { console.log(`Upload progress: ${percent}%`);});Axios handles the XMLHttpRequest details internally, so I get both Promise-based syntax and progress events.
Complete Example with Progress Bar
Here’s a full working example with a visual progress bar:
<!DOCTYPE html><html><head> <style> .progress-container { width: 100%; background: #e0e0e0; border-radius: 4px; margin: 10px 0; } .progress-bar { height: 20px; background: #4CAF50; border-radius: 4px; width: 0%; transition: width 0.3s; } </style></head><body> <input type="file" id="fileInput"> <button onclick="uploadFile()">Upload</button> <div class="progress-container"> <div class="progress-bar" id="progressBar"></div> </div> <p id="status"></p>
<script> function uploadFile() { const file = document.getElementById('fileInput').files[0]; if (!file) return alert('Please select a file');
const progressBar = document.getElementById('progressBar'); const status = document.getElementById('status');
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percent = Math.round((e.loaded / e.total) * 100); progressBar.style.width = percent + '%'; status.textContent = `${percent}% uploaded`; } });
xhr.addEventListener('load', () => { status.textContent = 'Upload complete!'; });
xhr.addEventListener('error', () => { status.textContent = 'Upload failed!'; });
xhr.open('POST', '/upload'); const formData = new FormData(); formData.append('file', file); xhr.send(formData); } </script></body></html>Comparison
| Feature | Fetch | XMLHttpRequest | Axios |
|---|---|---|---|
| Upload Progress | Not supported | Native support | Supported |
| Download Progress | Via streams (complex) | Native support | Supported |
| Promise-based | Yes | No (callbacks) | Yes |
| Bundle Size | Native | Native | ~13KB minified |
Common Mistakes to Avoid
- Not checking
lengthComputable: The progress event may not always have the total length available (e.g., with chunked encoding):
xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { // Safe to calculate percentage const percent = Math.round((event.loaded / event.total) * 100); } else { // Show bytes transferred instead console.log(`Uploaded: ${event.loaded} bytes`); }});- Forgetting abort handling: When implementing progress, don’t forget cancellation support:
const xhr = new XMLHttpRequest();// ... setup ...
// Cancel buttoncancelButton.addEventListener('click', () => { xhr.abort();});- Waiting for fetch to add this feature: Despite being a top feature request for years, there’s no timeline for when (or if) fetch will get native progress events.
The Reason
The fetch API was designed with streams in mind for the response body, but the request body upload wasn’t given the same treatment. The Streams API can theoretically track upload progress, but it remains experimental and complex to implement.
For now, XMLHttpRequest remains the only native way to track upload progress. If you’re already using axios, its onUploadProgress option is the simplest solution. If you want zero dependencies, wrap XMLHttpRequest in a Promise like I showed above.
Summary
In this post, I showed how to track file upload progress in JavaScript. The fetch API doesn’t support progress events natively, so use XMLHttpRequest with upload.onprogress for zero-dependency progress tracking, or axios for a cleaner Promise-based API. The key point is that despite fetch being the modern standard, XMLHttpRequest still has one feature that fetch lacks.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments