What preventDefault() Actually Does to Your Form Submission
preventDefault() cancels the browser's default action. On a form that means no reload and no request. Here's what reaches your server after you call it.
A form works fine. Then someone adds a submit handler, the page stops reloading, and submissions stop arriving. Nothing in the console, no error in the network tab, no failed request, because there is no request at all.
That is preventDefault() doing precisely what it was asked to do.
preventDefault() tells the browser not to take the default action for an event. On a form’s submit event, the default action is the whole submission: serialize the fields, build the request, send it to the action URL, and load whatever comes back. Cancel that, and the browser does none of it. Whether anything reaches your server after that is entirely up to the code you write next.
We run a form endpoint, which means we see the far side of that decision all day. This guide covers what preventDefault() is, and then the part the reference docs can’t tell you: what shows up at the other end once you’ve called it.
Table of Contents
- What preventDefault() does
- The form submit case
- What the server sees after you call it
- preventDefault vs stopPropagation vs return false
- When preventDefault does nothing
- submit() vs requestSubmit()
- Do you need it at all?
- A checklist when submissions stop arriving
What preventDefault() does
Most events have a default action the browser performs unless something intervenes. Clicking a link navigates. Ticking a checkbox toggles it. Right-clicking opens the context menu. Submitting a form sends a request and loads the response.
MDN describes the method as telling “the user agent that the event is being explicitly handled, so its default action, such as page scrolling, link navigation, or pasting text, should not be taken.” (MDN, Event.preventDefault())
form.addEventListener('submit', (event) => { event.preventDefault(); // The browser will not submit this form. // Anything that happens now is your job. });
The method takes no arguments and returns nothing. It works only on events that are cancelable, which you can check with event.cancelable. After it’s called, event.defaultPrevented reads true, which is occasionally useful when a second listener needs to know whether an earlier one already handled things.
The one thing it does not do
preventDefault() does not stop the event moving through the DOM. From the same MDN page: “The event continues to propagate as usual, unless one of its event listeners calls stopPropagation() or stopImmediatePropagation().”
This trips people up because the two ideas feel like one idea. They aren’t. Cancelling the default action and stopping the event’s journey up the tree are separate operations, and you can do either without the other. A submit handler that calls stopPropagation() and expects the page to stay put will watch it reload anyway.
The form submit case
Forms are where this method gets used most, and where the consequences are least visible.
The default action of submit is not a small thing you’re switching off. It’s a sequence: the browser walks the form, builds the form data set, encodes it, sends it to the URL in the action attribute using the method you specified, and then navigates to the response. Cancelling it removes all of that at once.
The usual reason to do this is that you want the page to stay where it is:
<form id="contact" action="https://www.formbackend.com/f/YOUR_TOKEN" method="post"> <input type="text" name="name" required> <input type="email" name="email" required> <textarea name="message"></textarea> <button type="submit">Send</button> </form>
const form = document.querySelector('#contact'); form.addEventListener('submit', async (event) => { event.preventDefault(); const response = await fetch(form.action, { method: 'post', body: new FormData(form, event.submitter), headers: { Accept: 'application/json' } }); if (response.ok) { form.reset(); showSuccess(); } else { showError(await response.json()); } });
That’s the shape of a correct handler. Every line of it exists because of something that goes wrong when it’s missing, which is the rest of this guide.
What the server sees after you call it
Here is the part the browser reference can’t cover, because it’s about the other end of the connection. These are the four patterns we see on our endpoint, in roughly the order they turn into support tickets.
Nothing arrives
The most common failure, and the quietest. preventDefault() runs, and then something between it and the fetch() call throws:
form.addEventListener('submit', (event) => { event.preventDefault(); const data = JSON.parse(someValue); // throws fetch(form.action, { method: 'post', body: data }); // never runs });
The browser’s submission is already cancelled. The replacement never fires. The visitor clicks Send, sees nothing happen, and usually clicks it again. No request is made, so there’s nothing in your network tab, nothing in your server logs, and nothing in your dashboard.
This is worth internalizing: preventDefault() is not reversible. Once the default action is cancelled, there is no way to ask the browser to go ahead with it after all. Your code owns the submission from that point forward, including the error handling. Put the preventDefault() call where it belongs: after you’ve done anything that might throw, or inside a handler where every path ends in either a request or a visible error message.
Two things arrive
The opposite problem. A scripted submission leaves the button live, so an impatient double-click sends two requests. The native form submission doesn’t have this failure mode, because the browser starts navigating away and the second click lands on a page that’s already leaving.
Our endpoint rate-limits submissions to 7 per minute per client. A double-click spends two of those seven on the same message. It also means a genuine mistake — a retry loop, a handler bound twice — gets throttled rather than silently duplicating a customer’s data a hundred times.
Disable the button while the request is in flight:
const button = event.submitter; button.disabled = true; try { await fetch(/* ... */); } finally { button.disabled = false; }
One caution, which is easy to get wrong: a disabled field is not submitted. If you disable an input rather than the button, its value is dropped from the form data set entirely. That’s covered alongside the other skip rules in our guide to what each input type sends to your server.
The response isn't what you expected
This one surprises people, and it’s a direct consequence of preventDefault() rather than a mistake in the handler.
Our endpoint content-negotiates. A native form submission is an HTML navigation, so it takes the HTML path: we store the submission and respond with a redirect, either to a _redirect_to value or to a thank-you page. A fetch() call is not a navigation, and unless you tell it otherwise it may well receive that same HTML response, at which point response.json() fails on markup and you get a parse error that looks nothing like the actual problem.
headers: { Accept: 'application/json' }
That header is the whole fix. It moves you onto the JSON path, where validation errors come back as a 422 with a parseable body instead of a redirect to a page you never wanted.
The general lesson generalizes past our endpoint: preventDefault() changes what the server sends back to you, not just how you send it. Any endpoint that behaves differently for navigations and background requests will behave differently the moment you stop letting the browser submit the form. Worth checking before you assume the payload is at fault.
The button is missing
A native browser submission includes the clicked submit button in the form data set. new FormData(form) does not.
That matters as soon as one form has more than one button:
<button type="submit" name="action" value="save">Save draft</button> <button type="submit" name="action" value="publish">Publish</button>
Submitted natively, the server receives action=save or action=publish. Submitted through a handler that builds new FormData(form), the server receives neither, and every submission looks identical. The logic that decides between saving and publishing has nothing to work with.
The constructor takes an optional second argument for exactly this. MDN: “If the submitter has a name attribute or is an <input type="image">, its data will be included in the FormData object (e.g., btnName=btnValue).” (MDN, FormData())
new FormData(form, event.submitter)
event.submitter is the button that triggered the submission, available on the SubmitEvent. It’s null when the submission wasn’t triggered by a button, which is one more reason to prefer requestSubmit() over submit(). More on that below. (MDN, SubmitEvent.submitter)
Two practical notes. The constructor throws a TypeError if what you pass isn’t a submit button, and a NotFoundError if the button isn’t a member of that form. And support is recent by the standards of form APIs: the submitter parameter landed in Chrome and Edge 112, Firefox 111, and Safari 16.4, covering roughly 92.7% of global browser usage (caniuse, accessed 25 August 2026). For the remaining tail, read the value off event.submitter and append() it yourself.
preventDefault vs stopPropagation vs return false
The three get used interchangeably in code that works by accident. They do different things.
| Call | Cancels the default action | Stops the event propagating |
|---|---|---|
event.preventDefault() |
Yes | No |
event.stopPropagation() |
No | Yes |
event.stopImmediatePropagation() |
No | Yes, including other listeners on the same element |
return false (jQuery handler) |
Yes | Yes |
return false (plain addEventListener) |
No | No |
That last row causes real bugs. Returning false from a listener added with addEventListener() does nothing whatsoever: the return value is discarded. The habit comes from jQuery, where it’s documented behavior: “Returning false from an event handler will automatically call event.stopPropagation() and event.preventDefault().” (jQuery .on() documentation)
For a form, you almost always want preventDefault() alone. Stopping propagation as well is a side effect you didn’t ask for, and it breaks any delegated listener higher up the tree: analytics, a dirty-form warning, a modal that closes on submit.
When preventDefault does nothing
Sometimes the call runs and has no effect at all. There are three reasons.
The event isn’t cancelable. MDN is blunt about it: calling preventDefault() for a non-cancelable event “has no effect.” Check event.cancelable if you’re unsure. The submit event is cancelable, so this isn’t usually the form case. But it is the case for synthetic events dispatched via dispatchEvent() without cancelable: true.
The listener is passive. Browsers changed the default for scroll-related events to improve scroll performance. Per MDN, the passive option defaults to true for the wheel, mousewheel, touchstart and touchmove events on Window, Document, and Document.body. “If a passive listener calls preventDefault(), nothing will happen and a console warning may be generated.” (MDN, addEventListener()) If you need to cancel one of those, pass { passive: false } explicitly.
The handler never ran. Covered next, and it’s the one that catches people out on forms.
submit() vs requestSubmit()
If some other code calls form.submit(), your submit handler will not run. Not “runs and gets ignored” — never runs. preventDefault() can’t cancel an event that was never fired.
MDN puts the distinction plainly: “submit() submits the form, but that’s all it does. requestSubmit(), on the other hand, acts as if a submit button were clicked. The form’s content is validated, and the form is submitted only if validation succeeds. Once the form has been submitted, the submit event is sent back to the form object.” (MDN, requestSubmit())
form.submit() |
form.requestSubmit() |
|
|---|---|---|
Fires the submit event |
No | Yes |
| Runs constraint validation | No | Yes |
| Submits if validation fails | Yes | No |
| Accepts a submitter button | No | Yes |
So form.submit() bypasses both your handler and every required attribute on the form. If a submission arrives with fields you were certain the browser would have caught, a stray submit() call is a good suspect. Client-side validation only runs on the path that checks it.
Use requestSubmit(). It has been Baseline widely available since September 2022, and it takes the submitter as an argument, so event.submitter is populated the way it would be on a real click.
Do you need it at all?
Worth asking, because the honest answer is often no.
A plain HTML form with an action and a method submits without a line of JavaScript. That path can’t be broken by a script error, a failed bundle, or a handler bound to an element that hadn’t rendered yet. It works on the first paint. Our endpoint accepts it directly and needs no CSRF token, so there’s genuinely nothing to wire up:
<form action="https://www.formbackend.com/f/YOUR_TOKEN" method="post"> <input type="email" name="email" required> <button type="submit">Subscribe</button> </form>
Add a redirect with a _redirect_to field and you have a complete flow — submit, store, thank-you page — with no preventDefault() anywhere. Those underscore-prefixed fields configure the submission rather than becoming part of it; the full set is in our special fields documentation.
Reach for preventDefault() when you want something the browser can’t give you: staying on the page, inline error messages, an upload progress bar, or a JSON payload. Those are good reasons. “Everyone writes forms this way” is not, and the scripted path costs you the no-JavaScript fallback, the automatic button state, and the submitter value unless you rebuild each one by hand.
The pattern that keeps both is progressive enhancement: write the form so it works natively, then let the script take over when it loads. If the script fails, the form degrades to a page reload instead of to nothing. We walk through that structure in more detail in submitting HTML forms with JavaScript, and the file upload version has the same shape.
A checklist when submissions stop arriving
Work down this list. It’s ordered by how often each one turns out to be the cause.
- Does the handler throw between
preventDefault()andfetch()? Wrap it and log. This is the most common cause by a distance, and it’s invisible in the network tab because no request is made. - Is there a
fetch()call at all? A handler that only validates and never sends is a form that never submits. - Is the
Acceptheader set? Without it you may get an HTML redirect where you expected JSON, and the parse error will point you somewhere unhelpful. - Is the button still enabled during the request? If so, expect duplicate submissions and rate-limit rejections.
- Are you passing
event.submittertoFormData? If not, the clicked button’s name and value never reach the server. - Did you disable an input rather than the button? Disabled fields are dropped from the form data set entirely.
- Is something calling
form.submit()? Then nosubmitevent fires, your handler is skipped, and validation is skipped with it. UserequestSubmit(). - Is the
actionURL right? If the request isn’t going where you think, nothing above matters.
preventDefault() is a one-line method with a one-line description, and almost everything written about it stops there. The part that costs an afternoon is what happens next: you have taken ownership of the submission, and the browser is no longer going to cover for you. Every guarantee it was quietly providing — the request gets sent, the button stops accepting clicks, the clicked button’s value goes along, an error is visible to the person filling in the form — is now something you have to provide yourself.
If you’d rather see exactly what your form sends without standing up a backend to inspect it, that’s the short version of what we do: point the form’s action at a FormBackend endpoint and every submission arrives with its raw keys and values, native or scripted. Useful while you’re debugging the handler, and it keeps working after you’ve stopped.
Frequently asked questions
What does preventDefault() do?
It tells the browser not to perform the default action associated with an event. On a link, the default action is navigating to the href. On a form's submit event, it is building the request, sending it, and loading the response. Calling preventDefault() cancels that step and nothing else: the event still propagates up the DOM, your handler still runs, and the rest of the page is unaffected. It only works if the event is cancelable.
Does preventDefault() stop a form from submitting?
It stops the browser from submitting the form. It does not stop the submission from happening if you send it yourself. Calling preventDefault() in a submit handler cancels the browser's native request, and from that point on nothing reaches your server unless your own code makes a fetch() call. If the handler throws before that call, the form silently does nothing and no submission is recorded anywhere.
What is the difference between preventDefault() and stopPropagation()?
They act on two different things. preventDefault() cancels the browser's default action for the event, but the event keeps travelling up through parent elements. stopPropagation() stops the event travelling to parent elements, but the default action still happens. A form submit handler that calls stopPropagation() instead of preventDefault() will still reload the page. In jQuery, returning false from a handler calls both.
Why is my preventDefault() not working?
Three common causes. The event is not cancelable, in which case the call has no effect at all. The listener is passive, which browsers apply by default to wheel, mousewheel, touchstart and touchmove on window, document and document.body; a passive listener that calls preventDefault() does nothing and may log a console warning. Or the form is being submitted by form.submit(), which does not fire a submit event at all, so your handler never runs.
Does new FormData(form) include the button that was clicked?
No. The submit button's name and value are left out unless you pass the button as the constructor's second argument: new FormData(form, event.submitter). A native browser submission includes the clicked button; the one-argument FormData constructor does not. If your server distinguishes save from publish by which button was pressed, that information disappears the moment you call preventDefault() and build the payload yourself.
Should I use form.submit() or form.requestSubmit()?
requestSubmit(), in almost every case. form.submit() submits the form and does nothing else: it skips constraint validation and never fires the submit event, so required fields go unchecked and your own submit handler is bypassed. requestSubmit() behaves as if a submit button were clicked, validates first, submits only if validation passes, and fires the submit event. It has been Baseline widely available since September 2022.
Do I need preventDefault() to submit a form?
No. A plain HTML form with an action and a method submits without any JavaScript, and that path is more reliable than the scripted one because it cannot be broken by a script error. Reach for preventDefault() when you need something the browser cannot give you: staying on the page after submitting, showing inline errors, or sending the data as JSON. If you do not need one of those, the native submission is the better default.
Keep reading
HTML Form Input Types: What Each One Sends to Your Server
All 22 HTML form input types, what each one actually sends to your server, and why fields go missing: unchecked boxes, disabled inputs, and enctype.
The HTML Form Action Attribute: A Complete Guide
What the HTML form action attribute does, what to put in it when you have no server, how formaction works, and a checklist for when your form action won't submit.
Backend as a Service Examples: Real-World BaaS Use Cases
Discover real-world backend as a service examples, from forms to databases. Learn how to choose the best BaaS for your project with practical comparisons.
Add a form backend to your site in minutes
Connect any HTML form to FormBackend and start collecting submissions — no backend code required.
Start free