W3docs

Javascript Fetch: download progress

Learn how to track download progress with the Fetch API using ReadableStream to read response body chunks incrementally.

The Fetch API is the modern way to make network requests in JavaScript, but await fetch(...) resolves as soon as the response headers arrive — long before the body has finished downloading. To show a progress bar for a large file you need to read the body incrementally and count bytes as they arrive. This chapter covers how to do exactly that with a ReadableStream, why Content-Length matters, how to build a progress UI, and the one thing fetch cannot do (upload progress).

Why fetch needs a stream for progress

When you write const data = await response.json(), the browser buffers the entire body internally and only hands it back when complete — there is no hook to observe the bytes mid-flight. The Response body, however, is exposed as a ReadableStream through response.body. By pulling chunks off that stream yourself, you can measure how much has arrived and update the UI on every chunk.

The recipe is always the same:

  1. Get a reader: const reader = response.body.getReader().
  2. Loop on reader.read(), which resolves to { done, value }value is a Uint8Array chunk.
  3. Add value.length to a running total and report progress.
  4. Keep the chunks, then reassemble them once done is true.

Reading the body as a ReadableStream

To know the percentage you also need the total size. The server should send a Content-Length response header; read it with response.headers.get('Content-Length'). It is often missing (chunked transfer encoding, gzip compression, or a server that simply omits it), so the example below falls back to a caller-supplied size.

javascript— editable

The function fetchWithProgress fetches data from a specified URL and tracks the download progress. It reads the response body in chunks using the ReadableStream interface and calls the onProgress callback with the received length and total content length. The downloaded data is then reconstructed from the chunks and returned as a decoded string.

Note

The function attempts to read the Content-Length header automatically. If the header is missing (common with chunked transfers or compression), it falls back to the fallbackSize parameter. In production, the developer or server must provide this fallback size. For large files, avoid buffering all chunks in memory; process them incrementally or use response.blob() for non-text responses.

Once the loop finishes you hold an array of Uint8Array chunks. To turn them back into usable data, copy them into a single buffer and decode (for text) or wrap them in a Blob (for binary files, images, downloads). See JavaScript Blob for working with binary data and triggering file downloads.

Displaying progress to users

A progress bar is just an element whose width tracks received / total. Pass an updateProgressBar callback in place of the logging one:

<body>
  <div id="progress-bar" style="width: 100%; background-color: #e0e0e0;">
    <div id="progress" style="width: 0; height: 20px; background-color: #76c7c0;"></div>
  </div>
  <div id="output"></div>
  <script>
    function updateProgressBar(received, total) {
      const progressElement = document.getElementById('progress');
      const percentage = Math.min(100, (received / total) * 100);
      progressElement.style.width = percentage + '%';
    }
    document.addEventListener('DOMContentLoaded', () => {
      const url = 'https://api.w3docs.com/uploads/media/default/0001/05/dd10c28a7052fb6d2ff13bc403842b797a73ff3b.txt';
      const size = 3_900_000; // fallback size
      // fetchWithProgress is defined in the previous code block
      fetchWithProgress(url, updateProgressBar, size)
      .then(data => {
        document.getElementById('output').textContent = 'File content: ' + data.slice(0, 1000) + '...';
      })
      .catch(err => console.error("Download failed:", err));
    });
  </script>
</body>
Warning

If you are downloading a very large file, avoid updating the UI too frequently. For example, instead of updating the progress bar for every single chunk, you can update it less frequently (e.g., every few chunks or based on a time interval). It helps keep your UI lightweight.

The HTML elements in the example above set up a progress bar to visualize the download progress and a preformatted text area to display the downloaded content. The progress-bar div serves as the container, and the progress div represents the actual progress of the download.

The JavaScript code updates the progress bar based on the received data length and total content length. The updateProgressBar function calculates the percentage of downloaded data and adjusts the width of the progress bar accordingly. The event listener triggers the fetchWithProgress function on page load, updating the progress bar and displaying the downloaded content in the output element.

Note: fetchWithProgress is defined in the previous snippet. In a real project, ensure it is in scope (e.g., via module imports, bundling, or a global script tag).

What fetch cannot do: upload progress

The stream technique only tracks download (response) progress. As of today there is no standard way to observe upload (request body) progress with fetch — the request body is not exposed as an observable stream in browsers. If you need a progress bar while sending a file, fall back to XMLHttpRequest, whose upload object fires progress events:

function uploadWithProgress(url, file, onProgress) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('POST', url);

    // upload.onprogress fires repeatedly while the body is sent
    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) {
        const percent = Math.round((event.loaded / event.total) * 100);
        onProgress(event.loaded, event.total, percent);
      }
    };

    xhr.onload = () => resolve(xhr.responseText);
    xhr.onerror = () => reject(new Error('Upload failed'));
    xhr.send(file);
  });
}

event.lengthComputable tells you whether event.total is known; always guard on it before computing a percentage.

Cancelling an in-flight download

A long download should be cancellable. Pass an AbortSignal to fetch and call controller.abort() to stop both the request and the stream loop — see Fetch: Abort for the full pattern.

Professional Tips

  • Handle Binary Data: TextDecoder only works for text. Use response.blob() or response.arrayBuffer() for binary files.
  • Throttle Progress Updates: Rapid chunk reads can block the UI thread. Throttle or debounce the progress callback.
  • Memory Management: Avoid storing all chunks in an array for large files. Process them incrementally.
  • Content-Length Fallback: The header is often missing. Always provide a reliable fallbackSize.
  • Optimize User Feedback: Providing real-time feedback on download progress improves user satisfaction and can make your application feel more responsive.
  • Leverage Browser Capabilities: Different browsers may have varying support for advanced features. Test your implementation across multiple browsers to ensure compatibility.
  • Simplify When Possible: For downloads where streaming progress isn't strictly required, response.arrayBuffer() offers a simpler way to reconstruct data without manually concatenating chunks.

With these insights and examples, you are now equipped to implement the Fetch API with download progress tracking in your projects, offering users a seamless and informative experience.

Conclusion

Mastering the Fetch API involves understanding not only how to make basic requests but also how to handle more advanced scenarios such as tracking download progress. By using ReadableStreams, we can monitor and provide feedback on downloads, significantly enhancing the user experience. Implementing these techniques will ensure your applications are robust, user-friendly, and capable of handling large data transfers efficiently.

Practice

Practice
What are the key steps to track download progress using the Fetch API?
What are the key steps to track download progress using the Fetch API?
Was this page helpful?