D
P
0
← All articles Baca dalam Bahasa Indonesia

JavaScript, DOM & Browser Animation

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

· · 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.