FormData in JavaScript
Collect named form fields, repeated values, and files with FormData, then send them safely with fetch or convert them on purpose.
- 01Read a formPredict which fields
new FormData(form)includes, and say why. - 02Change the entriesUse
append,set,delete,has,get,getAll, and loops without losing values that share a name. - 03Send or convert with careSend FormData with
fetch, add files, and see whyObject.fromEntriescan lose values.
FormData packs a form
A form can hold many kinds of fields. A field is one thing the user fills in or picks. For example:
- text boxes and email boxes
- checkboxes and radio buttons
- lists where you can pick more than one option
- submit buttons
- file pickers
You could read each field by hand. But each kind of field has its own rules, and it is easy to forget one. FormData is a browser object that reads the whole form for you. It uses the same rules the browser uses when it sends a form.
You make one with new FormData(form). The result is a list of entries. Each entry is a pair: a name and a value. The name comes from the field's name attribute. The value is what the user typed or picked. The entries stay in the same order as the fields in the form.
You can also pass the submit button that was clicked as a second argument. Then newer browsers add that button's name and value as an entry too.
A shipping clerk walks along your packing table. Every item with a label goes into the box. Several items can have the same label. Before the box ships, you can still add an item or swap items for new ones.
- 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)
FormData is a browser object that holds a list of entries in order. Each entry has a name and a value. A value is either text (a string) or a file. You can loop over the entries with for...of.
const fd = new FormData();fd.append("name", "Asha");fd.append("drink", "tea");console.log(fd.get("name"));console.log([...fd]);Line 1 makes an empty FormData object called fd. Here we don't pass a form, so it starts with no entries.
- Line 2 adds an entry with the name
nameand the valueAsha.appendmeans "add to the end". - Line 3 adds a second entry:
drinkwith the valuetea. - Line 4 reads the value stored under
name, so it printsAsha. - Line 5 copies every entry into an array with
[...fd]. Each entry shows as a small[name, value]pair.
fd. Each entry is a name and a value. Line 4 asks for the name name and gets back Asha.Line 5 is a good first check when a server says a field is missing. You see exactly what will be sent.
FormData only carries values. It does not check them. It doesn't know if an email is real, if a number is too big, or if a file is safe. Check the values before you send them. The server must check them again too.
Which fields get included?
INTERACTIVEThe browser does not take every element inside a form. It takes a field only when all of these are true:
- The field has a
name. The name becomes the entry's name. - The field is not
disabled. A disabled field is greyed out and switched off. - A checkbox or radio button is checked.
- In a list where you can pick more than one option, an option is picked. Each picked option becomes its own entry.
The browser's rules call these fields successful controls. A control is just another word for a form field.
new FormData(form) takes a field only if it has a name and is not disabled. A checkbox also has to be checked.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); }});Press a submit button. Then compare the list of entries with the fields you see. A file you pick stays on your computer. The playground only shows its name, size, and type.
Predict each result first, then press a submit button and check:
- A
readonlyfield is included. The user can't change it, but it is still switched on. - A disabled field is skipped.
- An unchecked checkbox is skipped.
- A field with no
nameis skipped. - Two checked checkboxes with the same name give two entries with that name.
- An empty text box is included. Its value is the empty string,
"". - A file picker with a name but no file chosen is included in current browsers. Its value is an empty file.
- Disabled input with a
name - Readonly text input with a
name - Unchecked checkbox
- Input with no
name - Two checked checkboxes named
topics - Clicked submit button with a
nameandvalue <select multiple>with two options picked- Empty text input with a
name
Sort each card: does the browser include this field, skip it, or include it only sometimes?
const publishButton = form.querySelector('[value="publish"]');const data = new FormData(form, publishButton);console.log(data.get("intent"));A form can have several submit buttons, like "Save draft" and "Publish". Each has the same name, intent, but a different value.
- Line 1 finds the button whose value is
publish. - Line 2 passes that button as the second argument to
new FormData. - Line 3 reads the
intententry and printspublish.
Without that second argument, no button is included.
append, set, get, getAll
STEP THROUGHFormData is not a plain object. In a plain object, each name holds one value. In FormData, many entries can share one name. This happens often:
- A group of checkboxes uses one name for every box.
- A
<select multiple>list, where you can pick more than one option, uses one name for every picked option. - Your own code can add another entry with the same name using
append.
const fd = new FormData();fd.append("topic", "forms");fd.append("topic", "files");console.log(fd.get("topic"));console.log(fd.getAll("topic").join(" + "));Lines 2 and 3 add two entries, both named topic. Line 4 uses get, which gives the first value for a name. It prints forms. Line 5 uses getAll, which gives an array of every value for a name. join(" + ") turns that array into one string, so it prints forms + files. Use getAll for checkbox groups and multi-pick lists.
Step through the code. Then switch line 4 between append and set and compare.
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(" | "));topic entries, forms and files. append adds a third one. set removes the old ones and keeps only the new one.The table below lists the methods that are easy to mix up. Read each row as: "when I write this, this happens, and I use it for this."
| When you write | What happens | Use it for |
|---|---|---|
append(name, value) | It adds one more entry at the end. Old entries with that name stay. | Checkboxes, names with many values, and extra files. |
set(name, value) | It removes every old entry with that name. Then it stores one new entry. | Fields that should have one value, when you want to replace it. |
get(name) | It gives the first value for that name, or null if there is none. | An email, a title, or the picked radio button. |
getAll(name) | It gives an array of every value for that name. | Checkbox groups and multi-pick lists. |
has(name) | It gives true if at least one entry has that name. | Checking for a field that may be missing. |
delete(name) | It removes every entry with that name. | Cleaning up before you send. |
You can also loop over the entries. Each of these walks through them:
for (const [name, value] of fd)gives each name and value.fd.entries()gives each[name, value]pair.fd.keys()gives each name.fd.values()gives each value.[...fd]copies every pair into an array.
The order is the order you added the entries in. For new FormData(form), it is the order of the fields in the form.
const fd = new FormData();fd.append("topic", "forms");fd.append("topic", "files");console.log(fd.has("topic"));fd.delete("topic");console.log(fd.has("topic"));Line 4 prints true, because two topic entries exist. Line 5 calls delete, which removes both of them, not just the first. So line 6 prints false.
The formdata event
An event is a signal the browser sends when something happens. Your code can listen for it. The browser sends a formdata event right after it builds a FormData object from a form. The event has a formData property, which is that same FormData object. So your listener can add entries to it.
form.addEventListener("formdata", (event) => { event.formData.append("source", "formdata-event"); console.log(event.formData.get("source"));}); new FormData(form);- Line 1 listens for the
formdataevent on the form. - Line 2 adds an entry named
sourcetoevent.formData. - Line 3 reads that entry back.
- Line 6 builds a FormData object from the form. That sends the event, so the listener runs and prints
formdata-event.
This is handy for an entry every form on a site should send, like an app version number or a form id.
The formdata event does not replace the submit event.
- Use
submitto stop the page from reloading, or to decide whether to send at all. - Use
formdatato look at or change the entries while they are being built.
Sending files and requests
INTERACTIVEA request is a message your page sends to a server. The body is the data inside that message. A FormData object can be the body. For a real upload you would write:
fetch("/profile", { method: "POST", body: formData })
fetch sends the request. POST means "I am sending data to you". The browser turns the FormData into a format called multipart form data. It keeps each entry in its own part, so text and files can travel together.
The parts are split by a boundary: a long random line of text the browser makes up. The browser also writes a Content-Type header that names the format and that boundary. A header is a short note sent along with the body. Do not write the Content-Type header yourself. You can't know the boundary, so the server can't split the parts.
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);Press Build the request.
This builds a Request object on this page. A Request is the message fetch would send. Then request.formData() reads the entries back out, like a server would. Nothing is sent over the network.
fetch, pass the same FormData as body. The browser sets the multipart boundary for you.A file stays on your computer until you send it. When you pick a file in the playground, the page only shows its name, size, and type.
You can also make file data in code. A Blob is a chunk of raw data, like the contents of a file. A File is a Blob with a file name. To add a Blob as a file, give it a file name as the third argument: 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());Some older servers want a simpler format called URL-encoded. It looks like the end of a web address: name=value pairs joined with &.
- Lines 2 and 3 add two text entries.
- Line 4 turns the FormData into a
URLSearchParamsobject. That object holds the same pairs in URL-encoded form. - Line 5 prints the text. A space becomes
+, so it printsname=Ada+Lovelace&topic=forms.
Use this only when every value is a string. It has no way to carry a real file.
Objects and repeated names
INTERACTIVEYou may see Object.fromEntries(formData) in examples. It turns the entries into a plain object. That works well when every name has one value.
But a plain object has room for only one value per name. If two entries share a name, the second one overwrites the first. The first value is lost, and you get no warning.
You have a list of answers. Each answer has a label. You copy them onto a sheet that has one line per label. Two answers have the label "topic". You write the first one, then write the second over it. Only the last one stays.
- 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
topic overwrites the first, so forms is lost.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
Inside FormData, two topic entries are fine. A value is lost only when you copy the entries into a plain object, which has room for one value per name.
age is a string in both results. Check it, then turn it into a number yourself.The helper function at the top of the playground code keeps every value:
- The first time it sees a name, it stores the value as it is.
- When it sees the same name again, it turns that property into an array and adds the new value.
Notice one more thing: age is a string in both results. FormData turns every value that is not a file into a string. If a field holds a number, check it and turn it back into a number yourself.
const fd = new FormData();fd.append("age", 12);const age = Number(fd.get("age"));console.log(age);console.log(typeof age);- Line 2 adds the number
12. FormData stores it as the text"12". - Line 3 reads the text and turns it into a number with
Number(...). - Line 4 prints
12. - Line 5 uses
typeof, which tells you a value's type. It printsnumber.
Still check the value first. Number gives NaN ("not a number") when the text is not a number.
const fd = new FormData();fd.append("name", "Asha");fd.append("topic", "forms");fd.append("topic", "files");console.log([...fd.keys()].join(", "));keys() gives every name, even repeated ones. So line 5 prints name, topic, topic. Use it to check which names you are about to send.
Where you will use this
Use FormData whenever a submit event gives you a form. For example:
- profile forms with a photo
- contact forms and support tickets with attachments
- tools that import a file
- checkout notes and admin dashboards
It goes well with two earlier lessons. From Form events, you use preventDefault() to stop the page from reloading. From Form validation, you use checkValidity() or reportValidity() to check the fields first.
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,
});
});- Line 2 stops the page from reloading.
- Line 3 checks the fields. If one is wrong, the browser shows a message and the function stops.
- Line 5 builds the FormData from the form and the clicked button.
- Line 6 adds one more entry: the user's time zone, such as
Asia/Kolkata. - Lines 8 to 11 send the FormData as the request body.
The next lesson, fetch & JSON, is about sending requests and reading the answers. Here, the body is already ready to send.
Common misconceptions
- “FormData is JSON.” It is its own kind of object. The browser usually sends it as multipart form data, not JSON.
- “Every field is included.” Disabled fields, fields with no name, and unchecked checkboxes and radio buttons are skipped.
- “Two entries with one name is a mistake.” It is normal. Checkbox groups and multi-pick lists work this way.
- “Object.fromEntries is always safe.” When a name repeats, it keeps only the last value.
- “I should set Content-Type myself.” Let the browser set it. Only the browser knows the boundary.
The table below pairs each mistake with what is really true. The last column says how to see it for yourself.
| The mistake | What is true | How can I check? |
|---|---|---|
| FormData is the same as a plain object. | It is a list of entries in order, with its own methods. | Try [...fd] and fd.getAll(name). |
| A readonly field is skipped. | A readonly field is included. Only a disabled field is skipped. | Use the playground in "Which fields get included?". |
| Files are stored as strings. | Files are stored as File objects. Each has a name, size, and type. | Pick a file in the playground and look at its row. |
| Numbers stay numbers. | Every value that is not a file becomes a string. | typeof fd.get("age") gives "string". |
Practice
5 EXERCISESPredict the output. This checks that you know how set differs 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 old colors and stores one green. So getAll gives only green.
Predict what line 4 prints after turning the entries into an object.
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 second value, b, is the one that stays.
Predict the exact text that is printed.
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. URLSearchParams joins the pairs with & and writes the space as +: name=Ada+Lovelace&topic=forms.
Say what is wrong, then open the fixed version.
await fetch("/upload", {
method: "POST",
headers: { "Content-Type": "multipart/form-data" },
body: formData,
});await fetch("/upload", {
method: "POST",
body: formData,
});Remove the Content-Type line. Line 3 has no boundary, so the server can't split the parts. Without it, the browser writes multipart/form-data with the right boundary.
Write the helper from "Objects and repeated names" 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 for a name is stored as it is. When the same name comes again, that property becomes an array, so every value is kept.
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 the oldtopicvalues?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)takes every field that has a name and is not disabled, in form order. Checkboxes and radio buttons must be checked.- Many entries can share one name. Use
getAllwhen you need every value. appendadds.setreplaces.deleteremoves every entry with that name.- A value is a string or a file. Turn numbers back into numbers yourself.
- Send FormData as the
fetchbody. Don't write theContent-Typeheader yourself.
Final definition: FormData is a list of name and value entries, in order. The browser fills it from a form using its own rules. You can read it, change it, turn it into other shapes, or send it.
Up next: fetch & JSON, where you send requests and read the answers.
Want to practice turning entries into objects first? See the Object.keys, values & entries lesson.