A <form> element already contains a complete, structured record of its own data. FormData exposes that structure directly to JavaScript, instead of requiring a hand-written object built field by field.
1Constructing FormData From A Real Form
new FormData(formElement) reads the current value of every field carrying a name attribute that would normally be submitted β text inputs, <select>, checked checkboxes and radios, and files from <input type="file"> β bundling them into a single object that mirrors exactly what a native, non-JavaScript form submission would send to the server.
This is a meaningful shift from manually querying each input and building a plain object by hand: it can't drift out of sync with the actual form markup, correctly respects disabled fields (excluded automatically, matching native submission behavior), and handles the full range of field types β including files β uniformly.
2A Map-Like API For Reading And Modifying Entries
get(name) returns the first value under a given field name; getAll(name) returns every value as an array, essential for repeated-name fields like checkbox groups or multi-selects where more than one entry can share the same name. append(name, value) adds a new entry without removing existing ones under that name (allowing duplicates), while set(name, value) replaces any existing entries under that name with a single new one.
delete(name), has(name), and iteration via entries()/keys()/values() (FormData is directly iterable) round out an API deliberately modeled on the Map/URLSearchParams family, so the mental model transfers directly if you've already used either of those.
3Files And Correct fetch() Submission
Selected files from <input type="file"> are included automatically as real File objects (themselves a Blob subtype with .name, .size, .type, and .lastModified), meaning a single FormData instance can carry both ordinary text fields and file uploads together β no separate upload request or base64-encoding step required for the common case.
Submitting via fetch(url, { method: "POST", body: formDataInstance }) automatically sets the correct Content-Type: multipart/form-data; boundary=... header, computing a unique boundary string used internally to separate each field and file in the request body. Manually setting Content-Type yourself is a common, request-breaking mistake β it omits that computed boundary, which the server-side multipart parser needs to correctly split the payload back into individual fields.
4The formdata Event, And Why This Beats Manual Serialization
The formdata event fires directly on a <form> at the exact moment its FormData representation is being constructed β whether from an actual user submission or an explicit new FormData(form) call β with event.formData giving direct access to append computed values (a client-side timestamp, an idempotency key, a hashed field) without mutating the DOM with a throwaway hidden <input> purely to smuggle a value into the payload.
Beyond convenience, using FormData directly avoids the overhead and correctness risk of manually serializing form state to JSON: JSON.stringify on a naively-built object silently mishandles files entirely (they can't be represented in JSON) and requires hand-written logic to correctly collect multi-value fields like checkbox groups β logic FormData already implements correctly, matching the platform's own native submission semantics exactly.
5Step-by-Step Breakdown
Your Form Already Knows Its Own Data. Manually building a payload object by reading each input's .value one field at a time is exactly the kind of bookkeeping HTML forms were designed to make unnecessary. FormData reads a real <form> element and produces a complete, submission-ready data structure in one line.
new FormData(form) Reads Every Named, Submittable Field. Passing a <form> element to the FormData constructor immediately reads the current value of every field with a name attribute β text inputs, selects, checked checkboxes/radios, and file inputs β into a single object, exactly mirroring what a native form submission would send.
Constructing FormData. What does new FormData(formElement) capture?
- βThe current value of every named, submittable field in the form
- βOnly text-type <input> fields, ignoring selects and checkboxes
- βThe form's raw HTML markup as a string
get/getAll/append/set/delete: A Map-Like API. FormData exposes get(name) for a single value, getAll(name) for every value under a repeated name (checkbox groups, multi-selects), and append/set/delete for adding or modifying entries β append allows multiple values under one key, while set replaces any existing ones.
append vs set. A form has three checked checkboxes all named "interests". Which method retrieves all three values?
- βgetAll("interests")
- βget("interests"), which returns all of them automatically
- βkeys("interests")
File Inputs Come Through As Real File Objects. FormData automatically includes files selected via <input type="file">, exposed as native File objects with .name, .size, and .type properties β no separate file-reading code path is needed alongside the rest of the form data when submitting via fetch().
File Inputs In FormData. What type of object does data.get("avatar") return for a file input?
- βA native File object, with .name, .size, and .type
- βA base64-encoded string of the file's contents
- βFile inputs aren't included in FormData at all
Submitting With fetch() Skips JSON.stringify Entirely. Passing a FormData instance directly as fetch()'s body automatically sets the correct multipart/form-data Content-Type header (including the boundary) and serializes every field, including files β critically, you must NOT manually set Content-Type, since fetch computes the required boundary string itself.
Submitting FormData With fetch(). Should you manually set a Content-Type: multipart/form-data header when sending FormData as a fetch() body?
- βNo β fetch() sets it automatically, including the required boundary string
- βYes, it must always be set manually or the request fails
- βOnly in Safari, as a browser-specific workaround
The formdata Event Lets You Modify The Payload Just Before Submission. The formdata event fires on a <form> right when its FormData is being constructed β from an actual submit or from new FormData(form) β letting you append computed or derived fields (a client-generated timestamp, a hashed value) into event.formData without touching the underlying HTML inputs.
The formdata Event. What's the advantage of appending a computed field inside the formdata event, versus adding a hidden <input> to the form?
- βIt adds the field to the payload without mutating the DOM with an extra hidden input
- βIt makes the network request measurably faster
- βThere's no real difference between the two approaches
FormData API Mastered. You now know how to read an entire form β including files β into a FormData object, use its get/getAll/append/set methods, submit it correctly with fetch() without touching Content-Type, and inject computed fields via the formdata event.
Name Your Form Fields. FormData reads values by each input's name attribute β an unnamed field is invisible to it.
Level Up π
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1FormData Doesn't Change Any Accessibility Behavior Of The Underlying Form
It's a read/serialization layer over an existing, already-accessible <form> β correct labeling, validation messaging, and focus management on the form itself remain entirely unaffected by how its data is later read via JavaScript.
Best Practices
Always Construct FormData From The Real <form> Element, Not A Hand-Built Object
This guarantees the payload can never drift out of sync with the actual form markup as fields are added, removed, or renamed over time.
Never Manually Set Content-Type When Submitting FormData Via fetch()
Let fetch() compute the multipart boundary itself β a manually-set header omits it and breaks server-side parsing.
Frequent Bugs
A checkbox group only returns one checked value in JavaScript despite multiple boxes being checked.
Use getAll(name) instead of get(name) β get() only returns the first value under a repeated field name.
A file upload request fails on the server with a parsing error after manually setting Content-Type: multipart/form-data.
Remove the manual Content-Type header entirely and let fetch() compute it (with the required boundary) automatically from the FormData body.
Real-World Examples
A Signup Form Submitted With fetch() And FormData
A complete signup flow, including an avatar file upload and a client-injected timestamp, submitted without JSON.stringify.
form.addEventListener("formdata", (e) => {
e.formData.append("clientTimestamp", Date.now().toString());
});
form.addEventListener("submit", async (e) => {
e.preventDefault();
const res = await fetch("/api/signup", { method: "POST", body: new FormData(form) });
});