D
P
0

CSS & Web Animation

`new FormData(...)` Throws “not of type HTMLFormElement”: Your Selector Grabbed the Wrapper DIV, Not the FORM

June 28, 2026·4 min read
`new FormData(...)` Throws “not of type HTMLFormElement”: Your Selector Grabbed the Wrapper DIV, Not the FORM

The signup form on a booking platform I built suddenly stopped working. You'd click submit, nothing got sent, and the console showed a single red line that made me squint:

Uncaught TypeError: Failed to construct 'FormData': parameter 1 is not of type 'HTMLFormElement'

The trigger was about as plain as it gets. I grabbed the form by its class and handed that element to FormData:

const form = document.querySelector('.signup-form');
const data = new FormData(form);

Nothing looked wrong. The class was correct, spelled right, and the element existed in the DOM. I even ran console.log(form) and got back a real element — not null. Yet FormData flatly refused it.

Why this happens

The key word is "HTMLFormElement". FormData is picky: its constructor only accepts an actual <form> element. Not a <div>, not a <section>, not whatever element happens to carry the same class. If you hand it anything that isn't a <form>, it throws a TypeError immediately.

So why did my querySelector return the wrong element when the class was right? Because the markup wrapped the real form in a div that shared the same class. I reopened the template, and here's what was actually there:

<div class="signup-form">
  <form id="signup">
    <input name="email" type="email" />
    <button type="submit">Sign up</button>
  </form>
</div>

See the problem? There are two different elements, but the signup-form class sits on the wrapper <div>, not on the <form>. Maybe it came from a styling component, maybe it was inherited from older markup — either way, the wrapper had stolen the class.

document.querySelector('.signup-form') returns the first element that matches in document order. And the first match is the wrapper <div>, because it sits on the outside and appears earlier. So my form variable really was valid and non-empty — it just held a <div>. FormData looked at it, realized it wasn't an HTMLFormElement, and bailed. The error was honest from the start; I was the one reading "form" as a guarantee that I was actually holding a form.

This is what makes the bug slippery. The selector "worked". No null. No typo. Everything looked right until you realize a class is not an identity — wrapper divs reuse the form's class all the time and silently shadow the real one.

The fix

The fix is to stop selecting by the ambiguous class and target the <form> element specifically. Since the form has an id, the cleanest move is to select by that id:

const form = document.querySelector('#signup');
const data = new FormData(form);

Now that selector can only match one element — <form id="signup"> — and FormData accepts it without complaint.

If for some reason you have to keep using that class, force the selector to match only a form element by prefixing the tag:

const form = document.querySelector('form.signup-form');
const data = new FormData(form);

form.signup-form means "a <form> element that also has the class signup-form". The wrapper div can never slip through this filter, because it isn't a <form>. Even if the markup later changes and that class keeps showing up everywhere, this selector still locks onto the real form.

And if you're inside an event handler — say, on submit — there's an even sturdier option: walk up from the element that fired the event to the nearest form with closest:

form.addEventListener('submit', (e) => {
  e.preventDefault();
  const realForm = e.target.closest('form');
  const data = new FormData(realForm);
});

e.target.closest('form') climbs up from the target until it finds the nearest <form>, without caring about classes at all. It's the most robust approach when your markup structure shifts often or is outside your control.

I went with the #signup version in the end. One line, obvious intent, and it'll never get confused with any wrapper div.

The takeaway

FormData needs a genuine <form> element — full stop. It won't pull fields out of a <div>, no matter how nice that div's class looks. So when you build a FormData object, don't select the form by a class that other elements might also wear. Target it by #id or by tag (form...), not by a class the wrapper has borrowed.

The broader lesson: in the DOM, a selector that "works" doesn't mean it found the element you meant. Classes get reused freely; tags and ids are far more honest about what a thing is. When an API demands a specific kind of element, select that element in an equally specific way — so the error doesn't have to teach you slowly, the way this one taught me.