FormData JavaScript: Forms, Files, and Fetch

Aug 21, 2026·23 min read

A registration form can contain text, several selected interests, two uploaded files, an unchecked option and more than one submit button, but only some of those controls become request fields.

FormData is an ordered list of string and File entries that JavaScript can build from a form and send with fetch as a multipart request without manually encoding file bytes or boundaries.

What FormData is and when to use it

FormData represents the fields of a form as an ordered entry list. Every entry has a string name and a value that is either a string or a File.

One ordered entry listfirstlastdisplayNameMayaStringtopicsjavascriptStringsamplesportfolio.pdfFiletopicsperformanceString
FormData keeps ordered entries whose values are strings or files.

An entry list differs from a plain object because names can repeat. A registration form can contain three checked boxes named topics, and FormData keeps three separate topics entries in their original order.

That model fits files too. A file input contributes a File, while a text input contributes a string. The FormData reference covers the API on its own; this article follows one form all the way into a request.

The expected media type matters. FormData prepares a payload, but it does not choose the server endpoint, field names, validation rules, upload limits, storage policy or multipart parser.

FormData models a submitted form, not the entire form element. Controls must pass the browser’s submission rules before they enter the list.

From control to submitted entryA form controlHas a nonemptyname?yesEnabled?yesSelected, checked,or submitter?yesFormData entryNotsubmittednonono
Each control must pass the browser’s submission gates.

Create FormData from a form

Start with one registration form containing text, repeated checkboxes, a multiple-file input and two named submit buttons:

<form id="registration-form">
  <label>
    Display name
    <input name="displayName" value="Maya">
  </label>

  <label>
    Email
    <input type="email" value="[email protected]">
  </label>

  <label>
    Team code
    <input name="teamCode" value="ALPINE" disabled>
  </label>

  <fieldset>
    <legend>Topics</legend>
    <label>
      <input type="checkbox" name="topics" value="javascript" checked>
      JavaScript
    </label>
    <label>
      <input type="checkbox" name="topics" value="css">
      CSS
    </label>
    <label>
      <input type="checkbox" name="topics" value="performance" checked>
      Performance
    </label>
  </fieldset>

  <label>
    Attendance
    <select name="attendance">
      <option value="remote" selected>Remote</option>
      <option value="office">Office</option>
    </select>
  </label>

  <label>
    Work samples
    <input type="file" name="samples" multiple>
  </label>

  <button type="submit" name="action" value="preview">Preview</button>
  <button type="submit" name="action" value="register">Register</button>
</form>

The controls look like one group on the page, but the browser tests each one separately. These are the successful-controls rules that determine the entry list:

Form controlIncluded?Reason
displayName text inputYesIt has a nonempty name and is enabled
Email inputNoIt has no name
teamCode inputNoIt is disabled
Checked topics boxesYesEach checked box contributes one entry
Unchecked topics boxNoUnchecked checkboxes are excluded
Selected remote optionYesA selected, enabled option contributes an entry
Unselected office optionNoOnly selected, enabled options contribute entries
Chosen samples filesYesEach selected file contributes an entry
preview buttonSometimesIt is included only when it is the designated submitter
register buttonSometimesIt is included only when it is the designated submitter

Radio buttons follow the checkbox rule: an unchecked radio button contributes nothing. A select can contribute more than one entry when it allows multiple selections, because every selected and enabled option uses the select element’s name.

The submit event tells you which button initiated submission through event.submitter. Pass that button as the optional second constructor argument:

The button that was actually usedRegistration formdisplayName = MayaPreviewRegisterevent.submitteris RegisterFormDataaction=register
The submitter argument identifies which button becomes form data.
const form = document.querySelector('#registration-form');

form.addEventListener('submit', (event) => {
  event.preventDefault();

  const data = new FormData(form, event.submitter);

  for (const [name, value] of data) {
    console.log(name, value);
  }
});

If the reader presses Register after selecting portfolio.pdf and profile.png, the list contains displayName, two topics entries, attendance, two samples entries and action=register. The unnamed email, disabled team code, unchecked CSS box and Preview button stay out.

Without the second argument, neither submit button contributes its name and value. That detail matters when two buttons send the same form to different actions. Form submission events and methods explains the surrounding submission flow, while form properties and methods covers how JavaScript reaches the controls.

Read and change FormData entries

A FormData object keeps duplicate names as distinct entries. That rule connects append, set, get and getAll.

Use append to add one more value:

const data = new FormData();

data.append('topics', 'javascript');
data.append('topics', 'performance');
data.append('guests', 2);

The list now has two topics entries followed by guests. The number 2 becomes the string "2" immediately, because values that are neither strings nor Blob objects are converted to strings.

get reads the first matching value, while getAll reads every matching value:

const data = new FormData();

data.append('topics', 'javascript');
data.append('topics', 'performance');

console.log(data.get('topics'));
console.log(data.getAll('topics').join(', '));
console.log(data.getAll('missing').length);
javascript
javascript, performance
0

getAll returns an empty array when the name is absent. That makes it the correct read for checkbox groups, multiple selects and multiple-file fields.

Now replace the repeated values:

const data = new FormData();

data.append('topics', 'javascript');
data.append('topics', 'performance');
data.set('topics', 'forms');

console.log(data.getAll('topics').join(', '));
forms

set removes all existing topics entries and installs one replacement. append preserves the list; set rewrites that name.

Two different ways to change one nameBeforejavascriptperformanceappend cssAfter appendjavascriptperformancecssset formsAfter setformsBoth rows sharethe name topics
Append grows a repeated-name group; set replaces the whole group.

The remaining methods inspect or remove entries:

  • has(name) reports whether at least one matching entry exists.
  • delete(name) removes every entry with that name.
  • entries() iterates over [name, value] pairs.
  • keys() iterates over names, including repeated names.
  • values() iterates over string and File values.
  • for...of over FormData produces the same pairs as entries().

For a quick inspection, read both the type and value:

for (const [name, value] of data) {
  if (value instanceof File) {
    console.log(name, value.name, value.size, value.type);
  } else {
    console.log(name, value);
  }
}

A printed entry list is the first useful checkpoint when a field disappears. It separates form construction bugs from request and server bugs.

Send text and files with fetch

The canonical implementation uses the form unchanged, keeps its repeated entries, adds a generated text report as a Blob, and sends everything with fetch:

const form = document.querySelector('#registration-form');
const status = document.createElement('p');

status.setAttribute('role', 'status');
form.insertAdjacentElement('afterend', status);

form.addEventListener('submit', async (event) => {
  event.preventDefault();
  status.textContent = 'Sending registration…';

  const data = new FormData(form, event.submitter);

  const report = new Blob(
    [
      `Display name: ${data.get('displayName')}\n`,
      `Topics: ${data.getAll('topics').join(', ')}\n`,
      `Attendance: ${data.get('attendance')}\n`,
    ],
    { type: 'text/plain' }
  );

  data.append('summary', report, 'registration-summary.txt');

  try {
    const response = await fetch('/registrations', {
      method: 'POST',
      body: data,
    });

    if (!response.ok) {
      throw new Error(`Registration failed with HTTP ${response.status}`);
    }

    const result = await response.json();
    status.textContent = `Registration ${result.id} saved.`;
  } catch (error) {
    status.textContent = error.message;
  }
});

The file input supplies File objects under samples. The script adds a Blob under summary and supplies the filename registration-summary.txt.

That third argument matters for a Blob. Without it, the reported filename defaults to "blob". A File uses its original filename by default, although append can supply a replacement filename for either value.

Do not set the Content-Type header in this fetch call.

When body is FormData, the browser encodes the entries as multipart/form-data and generates a boundary. It also puts that boundary in the Content-Type header. A manually written header such as multipart/form-data omits the matching boundary and can leave the server unable to split the body into parts.

One generated token binds the requesttogetherBrowser generatesboundary = A7x9Request headermultipart/form-data;boundary=A7x9Names the separatorEncoded body—A7x9part 1—A7x9part 2
The browser generates one boundary shared by the header and body.

The worked request has a direct trace. Suppose the user chooses example.txt and diagram.png, then activates the register button. The ordered entry list immediately before fetch is:

displayName = "Maya"
topics = "javascript"
topics = "performance"
attendance = "remote"
samples = File("example.txt")
samples = File("diagram.png")
action = "register"
summary = File("registration-summary.txt")

The browser generates the actual boundary and wire bytes. The following boundary and file bodies are illustrative, but they show the corresponding request structure; each line break on the wire is CRLF:

Content-Type: multipart/form-data; boundary=example-boundary-7MA4YWxk

--example-boundary-7MA4YWxk
Content-Disposition: form-data; name="displayName"

Maya
--example-boundary-7MA4YWxk
Content-Disposition: form-data; name="topics"

javascript
--example-boundary-7MA4YWxk
Content-Disposition: form-data; name="topics"

performance
--example-boundary-7MA4YWxk
Content-Disposition: form-data; name="attendance"

remote
--example-boundary-7MA4YWxk
Content-Disposition: form-data; name="samples"; filename="example.txt"
Content-Type: text/plain

<bytes of example.txt>
--example-boundary-7MA4YWxk
Content-Disposition: form-data; name="samples"; filename="diagram.png"
Content-Type: image/png

<bytes of diagram.png>
--example-boundary-7MA4YWxk
Content-Disposition: form-data; name="action"

register
--example-boundary-7MA4YWxk
Content-Disposition: form-data; name="summary"; filename="registration-summary.txt"
Content-Type: text/plain

<bytes of registration-summary.txt>
--example-boundary-7MA4YWxk--

A multipart parser might expose the result like this, although the exact server API varies:

fields.displayName = "Maya"
fields.topics = ["javascript", "performance"]
fields.attendance = "remote"
fields.action = "register"
files.samples = [File("example.txt"), File("diagram.png")]
files.summary = File("registration-summary.txt")

The repeated topics parts remain separate, and the generated boundary in the header matches every delimiter in the encoded body.

Open the browser Network panel and inspect the request headers and submitted fields when this chain breaks. Redact real filenames and personal data before sharing a screenshot, and never copy a boundary from another request.

The response check belongs in the same implementation. fetch can fulfill its promise when the server returns an HTTP error such as 404, so response.ok must be checked explicitly. Fetch covers the request API in more detail, and File and FileReader covers reading selected files in the browser.

For a wider treatment of promises, modules and request code, Async, Modules & Modern JavaScript carries the same flow into the surrounding language features.

Convert FormData without losing values

Object.fromEntries looks like a natural conversion because FormData iterates over name and value pairs:

const data = new FormData();

data.append('displayName', 'Maya');
data.append('topics', 'javascript');
data.append('topics', 'performance');

const object = Object.fromEntries(data);

console.log(object.displayName);
console.log(object.topics);
Maya
performance

The object has one property named topics, so the later entry replaces the earlier one. This conversion is suitable only when the form is known to use unique names.

Many entries cannot fit one object keyFormDatatopics: javascripttopics:performancePlain objecttopics:performanceThe earlier javascript value has no second slot.
Converting to an object collapses repeated names into one property.

A duplicate-preserving reducer must decide whether each property holds one value or an array. This standalone alternative keeps every repeated value:

function formDataToObject(data) {
  const result = Object.create(null);

  for (const [name, value] of data) {
    if (!Object.hasOwn(result, name)) {
      result[name] = value;
      continue;
    }

    if (!Array.isArray(result[name])) {
      result[name] = [result[name]];
    }

    result[name].push(value);
  }

  return result;
}

const data = new FormData();
data.append('displayName', 'Maya');
data.append('topics', 'javascript');
data.append('topics', 'performance');
data.append('__proto__', 'first');
data.append('__proto__', 'second');

const object = formDataToObject(data);

console.log(object.displayName);
console.log(object.topics.join(', '));
console.log(object.__proto__.join(', '));
Maya
javascript, performance
first, second

That preserves duplicates, but files need another decision. The resulting object can still contain File objects, and turning that object into JSON does not turn file bytes into useful JSON data.

Choose a file policy before calling JSON.stringify. The application might reject files for that conversion, upload them separately and store returned identifiers, or extract selected metadata such as the filename and type. FormData itself does not choose among those jobs.

URLSearchParams has the same repeated-name shape for strings, and append can preserve repeated string values. It cannot preserve uploaded file bytes as files. Convert to it only when every entry is a string and the server expects URL-encoded data.

This is the conversion rule:

  • Use Object.fromEntries(data) only for known unique names and an accepted file policy.
  • Use a reducer when repeated names must become arrays.

The strings guide covers the conversions that happen once a value must become text.

Debug the FormData bugs that waste time

Start with the symptom, then inspect the earliest stage that can produce it.

Locate the first place the data disappearsA field is missingIs it in the printedFormData?noCheck the formname • disabledchecked • submitteryesIs it in the Networkmultipart body?noCheck request setupyesCheck server parser
Find the first stage where the expected entry is missing.

A missing text field usually begins in the form. Check for a nonempty name, then check whether the control is disabled. An id labels a DOM element for JavaScript and CSS, but the FormData entry uses name.

A missing checkbox or radio value has another first check: is the control checked? Unchecked controls are absent, so FormData does not contain a string such as "false" unless the application adds one itself.

A missing submit action points to construction. Use new FormData(form, event.submitter) when the endpoint needs the initiating button’s name and value.

A checkbox group reduced to one value points to reading or conversion. get('topics') returns the first entry, set('topics', value) replaces the group, and Object.fromEntries(data) collapses repeated names into one object property. Inspect getAll('topics') before conversion.

Missing files can begin at the input or at the server. First inspect the file input’s files.length. Then inspect the FormData entry and confirm that it is a File with the expected filename. An unselected successful file input can contribute an empty File with an empty filename and body, so the type check alone is insufficient; a selected file can also legitimately contain zero bytes. Finally, inspect the Network panel and confirm that the server endpoint accepts multipart/form-data and has a multipart parser configured for the expected field names.

A malformed multipart request often has an explicitly written Content-Type header. Remove that header and let fetch generate both the boundary and the matching encoded body.

An empty server body does not prove that the browser sent nothing. First inspect the Network panel. If the request contains the expected multipart fields, the remaining work is at the endpoint or parser rather than in FormData.

Finally, distinguish transport failure from HTTP failure. A caught network error and an HTTP 422 are not the same result. Check response.ok, record response.status, and read the response body according to the endpoint’s documented format.

For requests that cross origins, the server also has to permit the request under the browser’s cross-origin rules. Fetch: Cross-Origin Requests covers that separate layer.

FormData method and decision reference

The methods all operate on the same ordered entry list:

APIResult
append(name, value, filename?)Adds another entry at the end
set(name, value, filename?)Replaces all entries with that name
get(name)Returns the first value or null
getAll(name)Returns every value or an empty array
has(name)Reports whether the name exists
delete(name)Removes every entry with that name
entries()Iterates over name and value pairs
keys()Iterates over names
values()Iterates over values
for...ofIterates over the same pairs as entries()

Choose FormData when files or repeated fields must reach an endpoint expecting multipart data. Choose JSON when the endpoint expects JSON and the application has decided how every value, especially every file, becomes JSON data. Choose URLSearchParams when all values are strings and the endpoint expects URL-encoded fields.

The payload must fit the endpointServer expectsmultipartform dataJSONURLencodedFormDatafilesrepeated namesJSONnested datatyped valuesURL paramsstrings onlyno file bytes
Match the payload representation to the endpoint’s expected encoding.

Also consider the data shape and value types: JSON directly represents nested objects, arrays, numbers, booleans and null, while FormData and URLSearchParams need an explicit flattening or stringification convention. Parameters that identify or filter a resource may belong in the URL query rather than the request body.

Frequently asked questions

What is FormData in JavaScript?
FormData is an ordered list of named values for submitting form fields. Each value is a string or a File, and the same name can appear more than once.
How do I send FormData with fetch?
Pass the FormData object as the fetch body. Do not set the Content-Type header yourself, because the browser generates the multipart boundary that must match the encoded body.
Why is a form field missing from FormData?
The control may have no name, be disabled, be an unchecked checkbox or radio button, or be a button that was not passed as the submitter. Inspect the FormData entries before sending the request.
Can I convert FormData to a JavaScript object?
Object.fromEntries converts FormData to an object, but repeated names collapse to one property. Files also need an explicit conversion policy if the object will become JSON.
What is the difference between append and set in FormData?
append adds another entry even when the name already exists. set removes every existing entry with that name and replaces them with one new entry.