FormData in JavaScript
Collect named form fields, repeated values, and files with FormData, then send them safely with fetch or convert them on purpose.
- 01Pack a formPredict which controls
new FormData(form)includes and why. - 02Edit the packageUse
append,set,delete,has,get,getAll, and iteration without losing repeated names. - 03Send or convert intentionallySend multipart bodies, include files, and avoid
Object.fromEntriessurprises.
FormData packs a form
A form often has more than one kind of value: text, email addresses, checkboxes, radio buttons, multi-select choices, submit buttons, and files. Reading each control by hand is possible, but it is easy to forget a rule. FormData lets the browser apply the same rules it uses for a real form submission.
The main move is new FormData(form). It creates an ordered collection of name → value entries. If you pass the clicked submit button as the second argument, newer browsers can include that button’s name=value too.
Picture a shipping clerk walking across your form. Every enabled control with a label-like name gets packed. A checkbox group can put several items under the same label, and you can still add or replace items before shipping.
- In real life: A labeled item in the box
- In JavaScript: a
name → valueentry - In real life: The packing table
- In JavaScript: the form element
- In real life: Add another item
- In JavaScript:
append(name, value) - In real life: Replace all items with that label
- In JavaScript:
set(name, value)
Where the analogy stops: Real boxes do not have iteration order or getAll; FormData is still a JavaScript object with precise methods.
FormData is an iterable browser object containing ordered form entries whose values are strings or File/Blob objects.
What gets included?
INTERACTIVEThe browser does not pack every element inside a form. It collects successful controls: named controls that are not disabled, plus control-specific details such as checked checkboxes and selected options.
form.addEventListener("submit", (event) => { event.preventDefault(); const form = event.currentTarget; const submitter = event.submitter; // Newer browsers can include the clicked submit button. const data = new FormData(form, submitter); for (const [name, value] of data) { console.log(name, value instanceof File ? value.name : value); }});Submit the real form, then compare the entries with the visible controls. Files stay on your machine; the playground only shows their name, size, and type.
Try these predictions: a readonly input is included, a disabled input is skipped, an unchecked checkbox is skipped, a nameless input is skipped, checked checkboxes with the same name create repeated entries, and an empty text field is included as "". An enabled named file input with no selection appears in current browsers as an empty File entry.
- Disabled input with
name - Readonly text input with
name - Unchecked checkbox
- Input without
name - Two checked checkboxes named
topics - Clicked submit button with
name=value <select multiple>with two selected options- Empty text input with
name
Sort each card by the browser’s successful-control rules.
append, set, get, getAll
STEP THROUGHFormData is not a plain object. Repeated names are allowed and common: checkbox groups use them, <select multiple> uses them, and your own code can create them with append.
Step through repeated names, then switch line 4 between append and set.
script
fd.append("topic", "forms");fd.append("topic", "files");fd.append("topic", "uploads");console.log(fd.get("topic"));console.log(fd.getAll("topic").join(", "));console.log([...fd.entries()].map(([name, value]) => name + "=" + value).join(" | "));| Method | What it does | Best for |
|---|---|---|
append(name, value) | Adds another value after existing entries with that name. | Checkboxes, multi-value keys, extra files. |
set(name, value) | Removes all old values for that name, then stores one new value. | Single-value fields you want to overwrite. |
get(name) | Returns the first value, or null. | Email, title, one selected radio value. |
getAll(name) | Returns an array of every value for that name. | Checkbox groups and multi-selects. |
has(name) / delete(name) | Checks for at least one value / removes every value for that name. | Optional values and cleanup before sending. |
Iteration is also part of the toolset. You can use for (const [name, value] of fd), fd.entries(), fd.keys(), fd.values(), or [...fd]. The order is the insertion order you created or the form’s control order.
Sending files and requests
INTERACTIVEA FormData object can be the body of a request. For a real upload you would write fetch("/profile", { method: "POST", body: formData }). Do not set the multipart Content-Type header yourself; the browser adds the boundary that matches the body.
async function previewRequest(formData) { const request = new Request("/profile", { method: "POST", body: formData, }); const parsed = await request.formData(); const multipart = await new Request("/profile", { method: "POST", body: formData, }).text(); return { parsed: [...parsed.entries()], startsWithMultipart: multipart.includes("form-data"), };}const avatar = new Blob(["hello"], { type: "text/plain" });const fd = new FormData();fd.append("avatar", avatar, "avatar.txt");const file = fd.get("avatar");console.log(file.name);console.log(file.size);console.log(file.type);Run the local request preview.
This creates a Request object in the browser and parses it with request.formData(). It does not fetch an external URL.
fetch, pass the same FormData as body and let the browser set the multipart boundary.Files are local until you send them. Selecting a file in the playground only lets the page show its name, size, and type. Generated data can be added too: formData.append("avatar", blob, "avatar.png").
const fd = new FormData();fd.append("name", "Ada Lovelace");fd.append("topic", "forms");const body = new URLSearchParams(fd);console.log(body.toString());new URLSearchParams(formData) is useful for old-style URL-encoded bodies when every value is a string. It is the wrong shape for real files.
Objects and repeated names
INTERACTIVEYou may see Object.fromEntries(formData) in examples. It is convenient for truly single-value forms, but it silently collapses repeated names. Objects are single-slot organizers: the last value for a property overwrites earlier ones.
FormData is a list of labeled answers. When you turn it into a plain object, each label has one line. If two answers use topics, the last one replaces the first.
- In real life: Several answers use the same label
- In JavaScript: Repeated FormData entries
- In real life: One answer line per label
- In JavaScript: One object property
- In real life: The last answer stays visible
- In JavaScript: The last value wins
Where the analogy stops: A form can have repeated answers. Plain Object.fromEntries does not make an array for them.
function formDataToObject(fd) { const result = {}; for (const [name, value] of fd) { if (Object.hasOwn(result, name)) { result[name] = Array.isArray(result[name]) ? [...result[name], value] : [result[name], value]; } else { result[name] = value; } } return result;} const fd = new FormData();fd.append("topic", "forms");fd.append("topic", "files");fd.append("age", 12);console.log(Object.fromEntries(fd).topic);console.log(formDataToObject(fd).topic.join(" + "));console.log(typeof formDataToObject(fd).age);- topic
files - age
12
Repeated names are safe inside FormData. They collapse only when you unpack them into a one-slot-per-name object.
age is still a string either way; convert numbers explicitly after validation.Also notice type conversion: non-Blob values become strings. If a form field represents a number, validate and convert it on purpose after you read it.
Where you will use this
Use FormData for profile forms, contact forms with attachments, import tools, checkout notes, support tickets, and admin dashboards where a submit event already gives you a form element. It pairs naturally with the Form events lesson’s preventDefault() and the Form validation lesson’s checkValidity() / reportValidity().
form.addEventListener("submit", async (event) => {
event.preventDefault();
if (!form.reportValidity()) return;
const data = new FormData(form, event.submitter);
data.append("clientTimeZone", Intl.DateTimeFormat().resolvedOptions().timeZone);
await fetch("/profile", {
method: "POST",
body: data,
});
});The next lesson, fetch & JSON, will focus on the request/response side. Here, the key is that the body is already prepared.
Common misconceptions
- “FormData is JSON.” It is an iterable form body, usually sent as multipart data.
- “Every input is included.” Disabled, nameless, unchecked checkbox/radio controls are skipped.
- “Repeated names are wrong.” They are how checkbox groups and multi-selects work.
- “Object.fromEntries is always safe.” It loses earlier repeated values.
- “I should set Content-Type.” Let the browser add the multipart boundary.
| Mistake | Better model | Quick check |
|---|---|---|
| FormData equals a plain object | It is an ordered iterable with methods. | Try [...fd] and fd.getAll(name). |
| Readonly means skipped | Readonly is included; disabled is skipped. | Use the pack-the-box playground. |
| Files are strings | Files are File/Blob objects with name, size, and type. | Inspect the file entry row. |
| Numbers stay numbers | Non-Blob values are stringified. | typeof fd.get("age") is string. |
Practice
5 EXERCISESPredict the output. This checks whether set feels different from append.
const fd = new FormData();
fd.append("color", "red");
fd.append("color", "blue");
fd.set("color", "green");
console.log(fd.getAll("color").join(" & "));const fd = new FormData();
fd.append("color", "red");
fd.append("color", "blue");
fd.set("color", "green");
console.log(fd.getAll("color").join(" & "));set removes both earlier colors and stores one green, so getAll contains only green.
Predict what the object conversion prints.
const fd = new FormData();
fd.append("tag", "a");
fd.append("tag", "b");
console.log(Object.fromEntries(fd).tag);const fd = new FormData();
fd.append("tag", "a");
fd.append("tag", "b");
console.log(Object.fromEntries(fd).tag);Object.fromEntries writes tag twice. The later value b wins.
Predict the exact query string.
const fd = new FormData();
fd.append("name", "Ada Lovelace");
fd.append("topic", "forms");
console.log(new URLSearchParams(fd).toString());const fd = new FormData();
fd.append("name", "Ada Lovelace");
fd.append("topic", "forms");
console.log(new URLSearchParams(fd).toString());Both values are strings, so URLSearchParams produces name=Ada+Lovelace&topic=forms.
Explain the bug, then reveal the safer version.
await fetch("/upload", {
method: "POST",
headers: { "Content-Type": "multipart/form-data" },
body: formData,
});await fetch("/upload", {
method: "POST",
body: formData,
});Remove the manual Content-Type. The browser adds multipart/form-data with the correct boundary.
Write the helper from the object section without looking back.
function formDataToObject(fd) {
const result = {};
// Fill this in.
return result;
}function formDataToObject(fd) {
const result = {};
for (const [name, value] of fd) {
if (Object.hasOwn(result, name)) {
result[name] = Array.isArray(result[name])
? [...result[name], value]
: [result[name], value];
} else {
result[name] = value;
}
}
return result;
}The first value stays simple. The second value for the same name upgrades that property to an array, preserving all repeated entries.
Check your understanding
7 QUESTIONSQuestion 1 of 7What does
new FormData(form)collect?Choose an answer to see the explanation.
Question 2 of 7What does
fd.getprint after two appends?Read the code, then predictconst fd = new FormData(); fd.append("topic", "forms"); fd.append("topic", "files"); console.log(fd.get("topic"));Choose an answer to see the explanation.
Question 3 of 7After
fd.set("topic", "uploads"), what happens to earliertopicvalues?Choose an answer to see the explanation.
Question 4 of 7Why should you not set the
Content-Typeheader when sending FormData withfetch?Choose an answer to see the explanation.
Question 5 of 7What does
Object.fromEntries(fd).topicprint?Read the code, then predictconst fd = new FormData(); fd.append("topic", "forms"); fd.append("topic", "files"); console.log(Object.fromEntries(fd).topic);Choose an answer to see the explanation.
Question 6 of 7Which FormData values are possible?
Choose an answer to see the explanation.
Question 7 of 7What does
typeof fd.getprint for age?Read the code, then predictconst fd = new FormData(); fd.append("age", 12); console.log(typeof fd.get("age"));Choose an answer to see the explanation.
Key takeaways
new FormData(form)collects named, enabled successful controls in form order.- Repeated names are normal; use
getAllor arrays when you need every value. appendadds;setreplaces;deleteremoves all values for a name.- Values are strings or File/Blob objects. Convert numbers intentionally.
- Send FormData as a
fetchbody without hard-setting multipartContent-Type.
Final definition: FormData is an ordered, iterable package of form entries that follows browser submission rules and can be inspected, edited, converted, or sent.
Up next: fetch & JSON.
Want to revisit object conversion first? The published Object.keys, values & entries lesson pairs well with this one.