File uploads can fail even when your code is working correctly. Real-world networks aren’t always reliable. A phone can lose signal, hotel Wi-Fi can disconnect during an upload, or a user might close the tab before the upload finishes.
If your file upload API isn’t prepared for these situations, users may see a loading spinner that never ends. This can quickly make them lose trust in your app.
This guide assumes you already have a basic upload flow with features like drag-and-drop, a progress bar, and file validation. We won’t cover how to build those again.
Instead, we’ll focus on what happens when an upload fails. You’ll learn how to detect failed uploads, retry them without causing more problems, and resume large uploads instead of starting again from the beginning.
We’ll use simple, beginner-friendly JavaScript examples and also look at how Filestack’s JavaScript SDK handles some of this out of the box.
Key Takeaways
- Most upload failures happen because of network issues, timeouts, or closed tabs, not because your code is wrong.
- Handle network and server errors differently instead of treating every error the same.
- Wait a little longer between each retry (exponential backoff), and limit how many times you retry.
- For large files, continue from the last uploaded chunk instead of starting from the beginning.
- Show clear error messages and keep the upload progress instead of resetting it to 0%. This helps users trust your app.
Before we look at how to handle failed uploads, here’s a quick refresher on why they happen in the first place.
Why Uploads Fail
We won’t go through every reason an upload can fail here. Why most file uploads fail and what to do about it already explains common causes in detail, from filename issues and server timeouts to poor network connections. It’s also worth reading JavaScript file upload API: expectations vs. reality if you’ve only tested your upload flow on fast office Wi-Fi. Real users may have much slower or less reliable connections. Here, we’ll focus on what happens after an upload fails: how to detect the failure and recover from it smoothly.
Now, before you retry an upload, you first need to know what actually went wrong.
Detecting a Failed Upload Reliably
Upload failures aren’t always easy to identify. Different types of errors can happen, and each one may need a different response.
Network Error vs. Server Error
A network error usually means the request didn’t get a response because the connection was lost. A server error means the server did respond, but returned an error status such as 500 or 503.
These errors shouldn’t always be handled the same way. A network error is usually worth retrying. But some server errors, such as a 400 caused by an invalid file type, won’t be fixed by retrying.
You can learn more about these status codes in the MDN guide to HTTP status codes.
Timeouts
Sometimes an upload doesn’t fail or succeed; it simply keeps waiting. To handle this, you can set a timeout yourself instead of waiting forever.
Here’s a simple example using fetch and AbortController, as explained on MDN’s AbortController page:
async function uploadWithTimeout(file, timeoutMs = 15000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
const formData = new FormData();
formData.append("file", file);
try {
const response = await fetch("/api/upload", {
method: "POST",
body: formData,
signal: controller.signal,
});
if (!response.ok) {
// Server responded, but with an error status
throw new Error(`Upload failed with status ${response.status}`);
}
return await response.json();
} catch (err) {
if (err.name === "AbortError") {
throw new Error("Upload timed out. Please try again.");
}
throw err; // some other network or server error
} finally {
clearTimeout(timer);
}
}
This function helps you handle three common upload problems: a request that takes too long, a server that returns an error, and a network connection that drops.
Once you know what type of failure happened, it’s much easier to decide whether you should retry the upload or handle the error in another way.
Now that you can detect a failure, you need to decide when to retry it automatically and when to stop.
Retry Logic That Doesn’t Make Things Worse
Retrying an upload sounds simple, but you need to do it carefully. Too many retries can send even more requests to a server that’s already having problems. In some cases, retries can also cause the same file to be uploaded twice.
Backoff, Not Immediate Retries
If an upload fails, retrying it immediately may lead to the same error because the network or server hasn’t had time to recover.
A better approach is exponential backoff. This means waiting a little longer after each failed attempt. For example, you might wait 1 second before the first retry, 2 seconds before the next, and then 4 seconds.
This gives the network or server some time to recover. You can read more about why this approach is useful in AWS’s write-up on exponential backoff and jitter.

Here’s a simple example:
async function uploadWithRetry(file, maxAttempts = 4) {
let attempt = 0;
while (attempt < maxAttempts) {
try {
return await uploadWithTimeout(file);
} catch (err) {
attempt++;
if (attempt >= maxAttempts) {
throw new Error("Upload failed after several attempts. Please try again later.");
}
const waitTime = 1000 * Math.pow(2, attempt - 1); // 1s, 2s, 4s...
await new Promise((resolve) => setTimeout(resolve, waitTime));
}
}
}
Set a Retry Limit
Usually, three to five attempts are enough. If the upload still fails, stop retrying and show a clear error message with a button that lets the user try again manually. Don’t keep retrying silently in the background.
Avoid Duplicate Uploads
Sometimes a request is slow but hasn’t actually failed. If you retry too soon, the same file could be uploaded twice.
One way to prevent this is to track the state of each file using values such as pending, in-progress, succeeded, and failed. Before retrying, check the current state to make sure another upload isn’t already running.
Some upload APIs also provide ways to identify duplicate requests, so the server can avoid processing the same upload more than once.
Once your retry logic is in place, the next step is making sure large files don’t have to start over after every failed attempt.
Resuming Instead of Restarting: Chunked and Resumable Uploads
Retrying a small file isn’t a big problem. But if a 2GB video fails near the end, starting the entire upload again can be frustrating.
For large files, restarting from 0% every time the connection drops isn’t a good experience. A user might wait several minutes, lose their connection for a moment, and then have to start all over again.
A better approach is to split the file into smaller chunks. These chunks can be uploaded one at a time or a few at the same time. You also keep track of which chunks have already uploaded successfully.
If the connection drops, you only need to upload the remaining or failed chunks instead of uploading the entire file again.

Here’s a basic example of how chunking works:
function splitIntoChunks(file, chunkSize = 5 * 1024 * 1024) {
const chunks = [];
let start = 0;
while (start < file.size) {
chunks.push(file.slice(start, start + chunkSize));
start += chunkSize;
}
return chunks;
}
async function uploadChunks(file) {
const chunks = splitIntoChunks(file);
const status = chunks.map(() => "pending");
for (let i = 0; i < chunks.length; i++) {
if (status[i] === "succeeded") continue; // already uploaded, skip on resume
try {
await uploadWithRetry(chunks[i]);
status[i] = "succeeded";
} catch (err) {
status[i] = "failed";
}
}
return status;
}
The important part here is the status array. It keeps track of which chunks were uploaded successfully and which ones failed.
So, if the connection drops, you don’t have to start the entire upload again. You can skip the completed chunks and upload only the ones that are still missing or failed.
If you don’t want to build and maintain this chunking system yourself, the Filestack upload API can handle this kind of upload process for you. And if you’re building your own upload endpoints, designing a JavaScript API is a useful next read for learning how to create an API that supports chunked and resumable uploads.
Even if your retry logic works in the background, users still need to understand what’s happening.
Giving Users Useful Feedback Mid-Failure
Two simple things can make a big difference:
Don’t Reset Progress to 0%
If chunk 4 fails but chunks 1 through 3 were already uploaded successfully, don’t move the progress bar back to 0%. Keep the progress based on the chunks that have already finished.
Show Clear and Specific Errors
A message like “Upload failed” doesn’t tell the user what went wrong or what they should do next. Instead, use messages like “Upload failed, check your connection and try again” or “This file is too large (max 500MB).” This helps users understand the problem and what they can do about it.
If you’re uploading multiple files at the same time, tracking the status of each file becomes even more important. Uploading multiple files using JavaScript explains how you can use the same approach for multiple files, including tracking retries for each file separately.
Once users can clearly see what’s happening during a failed upload, you also need to make sure these failure states work as expected before your app goes live.
Testing Failure Scenarios Before They Happen in Production
It’s better to find upload problems during testing than when a real user runs into them.
You don’t need an unreliable internet connection to test these situations. Your browser’s developer tools can simulate them for you.
- Throttle the connection. Chrome and Firefox DevTools have network throttling options such as “Slow 3G” in the Network tab. Use them to see how your retry and backoff logic works on a slow connection.
- Simulate an offline drop. DevTools also has an “Offline” option. Start uploading a file, switch to offline mode during the upload, and see how your app handles the failure.
- Test what happens when the tab closes. Start uploading a large file and close the tab before it finishes. When you open the app again, check whether it remembers which chunks were already uploaded or starts the whole upload again.

Testing these situations helps you find problems with retries, error handling, and resumable uploads before your users experience them.
If you haven’t built the basic upload flow yet, the step-by-step guide to HTML file upload using JavaScript is a good place to start before adding this failure-handling logic.
Getting the file onto the server is the final step for this article, but it’s often just one part of the complete file workflow.
What Happens Downstream Once an Upload Finally Succeeds
Once a file uploads successfully, most apps need to do something with it. For example, you might resize an image, convert a video to another format, or create a thumbnail.
If you’re working with images, simplifying image editing with a JavaScript SDK explains some common ways to transform uploaded images.
After processing, the file usually needs to be available to your app or users. This is often done using a CDN URL or signed URL, which lets the file be displayed, downloaded, or shared without uploading it again.
With the full upload and recovery flow covered, here are a few best practices to keep in mind while putting everything together.
Best Practices and Common Pitfalls
Here are a few important things to remember when handling failed uploads:
- Do handle network errors and server errors differently.
- Do use exponential backoff and stop retrying after 3–5 attempts.
- Do track the upload status of each file or chunk to prevent duplicate uploads.
- Do keep the progress bar based on how much of the file has actually uploaded.
- Don’t keep retrying in the background forever. Show an error message and give users a manual retry option.
- Don’t retry every server error. For example, a 400 error caused by an invalid file type won’t be fixed by trying again.
- Don’t restart large uploads from the beginning after a connection drop. Resume from the last successful chunk instead.
With these practices in place, your upload flow will be much better prepared for the network problems users face in the real world.
Conclusion
A file upload API shouldn’t only work when the internet connection is fast and stable. It should also be ready for slow networks, connection drops, and other common problems.
You don’t need to rebuild your entire upload system to handle these issues. Focus on four things: detect failures correctly, retry failed uploads with increasing wait times and a clear limit, split large files into chunks so they can resume after a connection drop, and keep users informed about what’s happening.
Getting these things right makes your upload experience much more reliable and user-friendly.
If you don’t want to build and maintain all of this yourself, Filestack’s JavaScript SDK handles retries, chunking, and resumable uploads as part of its upload API.
FAQ
Why does a file upload fail even when the code has no bugs?
Most upload failures happen because of real-world conditions, not problems in your code. A network connection might drop during an upload, especially on mobile; the server might time out while handling a large file, or the user might close the tab before the upload finishes.
How do I tell the difference between a network error and a server error in JavaScript?
A network error usually means the request didn’t get a response, often because the connection was lost. A server error means the server responded with an error status code. Handle them differently: network errors are usually worth retrying, while some server errors are not.
Should I retry a failed upload immediately or wait?
Instead of retrying immediately, wait a little and increase the wait time after each failed attempt. This is called exponential backoff. Retrying too quickly on a poor connection or overloaded server will often just fail again.
How do I avoid uploading the same file twice if a retry fires after a slow original request succeeds?
Track the status of each file, such as in-progress, succeeded, or failed, and check it before retrying. You can also use an idempotency key to help the server identify and ignore duplicate requests.
Do I need chunked uploads to handle large file failures well?
Not always, but it’s very useful for large files. It lets the upload continue from where the connection dropped instead of starting again from 0%, which is especially helpful on slow or unreliable networks.
Shefali Jangid is a web developer, technical writer, and content creator with a love for building intuitive tools and resources for developers.
She writes about web development, shares practical coding tips on her blog shefali.dev, and creates projects that make developers’ lives easier.
Read More →