How to Cancel an Upload with AbortController
Cancel an in-flight upload with AbortController so the user can abort a slow transfer, and the fetch releases the connection immediately.
On this page
Introduction
The hardest part of cancel in-flight upload is not writing the code — it is deciding what the code is allowed to reject. A AbortController setup that accepts everything is easy to ship and expensive to operate, because every malformed input becomes a production surprise. The discipline is to enumerate the shapes the system actually receives, encode them in AbortController, and let the library reject the rest loudly. This keeps the happy path fast and the failure path visible.
The cost of cancel in-flight upload without AbortController is paid in incidents, not in code. The code looks fine because the happy path works, and the happy path is what you demo. The failure path is what you operate, and the failure path is where AbortController earns its keep. Instrument the rejections, alert on the spikes, and let the metric tell you when the guard is misaligned with reality.
The sections below walk through the concrete decisions: where to put the abortcontroller check, how to keep it fast under load, what to do when the contract changes, and how to test the failure paths so they degrade on purpose instead of by accident.
Create a controller
Create a controller is the first decision because it determines what the rest of the AbortController pipeline can assume. A wrong choice here turns into cancel in-flight upload under load, and the invariant you thought AbortController guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Create a controller is the one most teams skip: drive abortcontroller to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
const ctrl = new AbortController();
fetch('/upload', { method: 'POST', body, signal: ctrl.signal });
button.onclick = () => ctrl.abort();
The javascript above is the shape to ship for cancel in-flight upload. It is small enough to audit and specific enough that the failure behavior is obvious. Adapt the constants to your traffic and latency budget, and resist the urge to handle cases the system has not seen yet.
Pass signal to fetch
The mechanism behind Pass signal to fetch is straightforward in AbortController, but the consequences ripple. A wrong choice here turns into cancel in-flight upload under load, and the invariant you thought AbortController guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Pass signal to fetch is the one most teams skip: drive abortcontroller to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Abort on user action
Abort on user action is where most AbortController implementations leak performance. A wrong choice here turns into cancel in-flight upload under load, and the invariant you thought AbortController guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Abort on user action is the one most teams skip: drive abortcontroller to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
test('abort throws AbortError', async () => {
const ctrl = new AbortController();
const p = fetch('/upload', { signal: ctrl.signal });
ctrl.abort();
await expect(p).rejects.toThrow();
});
Pin the behavior with this test. A AbortController setup that passes this test degrades on purpose instead of by accident, which is the difference between a managed failure and an incident.
Handle the AbortError
The hard part of Handle the AbortError is not the happy path — it is what happens when abortcontroller fails halfway. A wrong choice here turns into cancel in-flight upload under load, and the invariant you thought AbortController guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Handle the AbortError is the one most teams skip: drive abortcontroller to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Release resources
Release resources should be observable without a debugger. A wrong choice here turns into cancel in-flight upload under load, and the invariant you thought AbortController guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Release resources is the one most teams skip: drive abortcontroller to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Test cancellation
Test cancellation is a trade, not a winner. A wrong choice here turns into cancel in-flight upload under load, and the invariant you thought AbortController guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Test cancellation is the one most teams skip: drive abortcontroller to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Common pitfalls
The common pitfalls in cancel in-flight upload all reduce to one mistake: treating AbortController as a formality instead of a contract. The contract is the thing the rest of the system relies on, and a weak contract propagates weakness everywhere.
- Skipping the abortcontroller boundary so cancel in-flight upload enters unvalidated.
- Assuming AbortController is fast without measuring the tail latency under load.
- Letting cancel in-flight upload fail silently instead of failing loudly with a metric.
- Testing only the happy path so the abortcontroller failure surfaces in production.
- Sharing mutable state across requests so one bad input poisons the next.
Conclusion
Do not conflate the conclusion with the implementation. They look similar from the outside but the failure modes differ: AbortController fails fast and the caller knows, while an unchecked path fails slow and the caller guesses. Choose the one whose failure mode your system can absorb, and document the choice at the call site.
Self-check: write a test that drives abortcontroller to the failing condition and assert the system degrades gracefully. Then cancel in-flight upload is a property you maintain, not a feature you hope for.
Sources: AbortController, Fetch
FAQ
- Does abort close the socket?
- Yes. The browser sends a RST and releases the connection.
- Can I abort a presigned PUT?
- Yes. Pass the same signal to the S3 PUT fetch.
Report a problem with this articleOpens a pre-filled issue on GitHub.