🔄 One Disable = No Duplicate Orders
A user double-clicks “Submit” or clicks fast on a slow connection, and your Ajax call fires twice — creating two orders, two payments, or two database rows. This is one of the most common (and expensive) bugs in web forms.
🐞 The Problem
document.getElementById("payBtn").addEventListener("click", function () {
$.ajax({
url: "/api/checkout",
method: "POST",
data: JSON.stringify(cartData),
contentType: "application/json"
});
// Nothing stops the user from clicking again before this finishes --
// two identical POST requests both succeed.
});
🔍 Cause & Effect
Click 1 -> request sent, still "in flight" (network latency, server processing) Click 2 (0.3s later, impatient user) -> second request sent Both requests reach the server and are processed independently => duplicate order, duplicate charge, duplicate database row
✅ Fix 1 — Disable the Button Immediately (Simplest, Always Do This)
document.getElementById("payBtn").addEventListener("click", async function (e) {
const btn = e.target;
if (btn.disabled) return; // guard clause -- extra safety
btn.disabled = true;
btn.textContent = "Processing...";
try {
await $.ajax({ url: "/api/checkout", method: "POST", data: JSON.stringify(cartData), contentType: "application/json" });
} finally {
btn.disabled = false;
btn.textContent = "Pay Now";
}
});
✅ Fix 2 — Cancel the Previous Request With AbortController (For Search/Filter Ajax)
let controller;
document.getElementById("search").addEventListener("input", function (e) {
if (controller) controller.abort(); // cancel the previous in-flight request
controller = new AbortController();
fetch("/api/search?q=" + encodeURIComponent(e.target.value), {
signal: controller.signal
})
.then(res => res.json())
.then(renderResults)
.catch(err => { if (err.name !== "AbortError") console.error(err); });
});
✅ Fix 3 — Debounce for Keystroke-Triggered Ajax (Fewer Requests, Not Just Fewer Duplicates)
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const debouncedSearch = debounce((q) => {
fetch("/api/search?q=" + encodeURIComponent(q)).then(/* ... */);
}, 300); // waits 300ms after the user stops typing before firing
document.getElementById("search").addEventListener("input", (e) => debouncedSearch(e.target.value));
📌 Server-Side Safety Net (Never Trust the Frontend Alone)
Always add an idempotency key: generate a unique request ID client-side, send it in the request header, and have the server reject/ignore a second request with the same ID within a short time window. This protects you even if a user opens two browser tabs.
Disabling the button on click is a two-line fix that eliminates the single most expensive class of Ajax bug — duplicate payments — before it ever reaches your database.
