Skip to content

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:

ideal-progress.js
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:

xhr-upload.js
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);
});
}
// Usage
uploadWithProgress(file, '/upload', (percent) => {
console.log(`Upload progress: ${percent}%`);
updateProgressBar(percent);
});

The key parts:

  • xhr.upload.addEventListener('progress', ...) - captures upload progress
  • event.lengthComputable - checks if total size is known
  • event.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:

axios-upload.js
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;
}
// Usage
await 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:

upload-demo.html
<!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

FeatureFetchXMLHttpRequestAxios
Upload ProgressNot supportedNative supportSupported
Download ProgressVia streams (complex)Native supportSupported
Promise-basedYesNo (callbacks)Yes
Bundle SizeNativeNative~13KB minified

Common Mistakes to Avoid

  1. Not checking lengthComputable: The progress event may not always have the total length available (e.g., with chunked encoding):
safe-progress.js
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`);
}
});
  1. Forgetting abort handling: When implementing progress, don’t forget cancellation support:
xhr-abort.js
const xhr = new XMLHttpRequest();
// ... setup ...
// Cancel button
cancelButton.addEventListener('click', () => {
xhr.abort();
});
  1. 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