[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-formdata-file-upload-json-stringify-trap":44,"search-suggestions":273,"related-formdata-file-upload-json-stringify-trap":312,"code:js:true:9pr7n1":441,"code:js:true:1qck05p":442,"code:js:true:z5qmbn":443,"code:json:true:ka3arn":444,"code:js:true:xzx2bj":445,"code:js:true:dq5guf":446,"comments-01a0b426-701c-74eb-b94f-6cad6f72a710":447},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":53,"likeCount":19,"commentCount":19,"author":54,"vertical":59,"topic":60,"tags":63,"_count":75,"playground":77,"body":79,"bodyMd":257,"seo":258,"translationGroupId":261,"series":48,"podcastUrl":48,"verticalId":5,"thread":262,"assessments":264,"translations":269,"quiz":271},"01a0b426-701c-74eb-b94f-6cad6f72a710","formdata-file-upload-json-stringify-trap","JSON.stringify Is Quietly Deleting Your File Uploads",null,"An avatar upload form that passes every test, ships to production, returns 200 OK — and never actually uploads the picture. The bug is one function call that silently turns a File into an empty object, with no error anywhere.","\u002Fmedia\u002Fcovers\u002Fformdata-file-upload-json-stringify-trap.png",5,"2026-09-26T12:24:36.794Z",45,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},"javascript","JavaScript",[64,66,69,72],{"slug":61,"name":65,"color":48},"Javascript",{"slug":67,"name":68,"color":48},"webdev","Webdev",{"slug":70,"name":71,"color":48},"tutorial","Tutorial",{"slug":73,"name":74,"color":48},"html","Html",{"assessments":76},1,{"slug":46,"title":78},"FormData vs JSON.stringify — the file-eating trap",{"blocks":80,"version":76},[81,85,88,91,94,98,101,107,110,113,117,120,124,127,131,134,137,142,145,149,152,156,159,166,169,172,175,179,182,187,190,194,197,200,203,206,209,212,215,218,221,224,227,230,233,236,239,242,245,248,251],{"id":82,"html":83,"type":84},"b1","\u003Cp>QA signs off on the &quot;edit profile&quot; form. Change your display name, hit save, refresh — the new name is there. Ship it.\u003C\u002Fp>","paragraph",{"id":86,"html":87,"type":84},"b2","\u003Cp>Two days later: &quot;I uploaded a new profile picture and it just... didn&#39;t change?&quot; You check the network tab. The request fired. Status \u003Ccode>200 OK\u003C\u002Fcode>. The server logged a successful update. No red text anywhere. By every signal your tools give you, this worked.\u003C\u002Fp>",{"id":89,"html":90,"type":84},"b3","\u003Cp>It didn&#39;t. And the bug isn&#39;t in the upload handler, the server, or the image itself — it&#39;s already dead by the time the request leaves the browser.\u003C\u002Fp>",{"id":92,"html":93,"type":84},"b4","\u003Cp>\u003Cstrong>Guess before you scroll:\u003C\u002Fstrong> the file never made it into the request body in the first place. Not corrupted. Not rejected. Just — never there.\u003C\u002Fp>",{"id":95,"html":96,"text":96,"type":97,"level":31},"b5","The code that looks completely fine","heading",{"id":99,"html":100,"type":84},"b6","\u003Cp>Here&#39;s roughly what shipped:\u003C\u002Fp>",{"id":102,"code":103,"type":104,"language":105,"highlight":106},"b7","form.addEventListener(\"submit\", async (e) => {\n  e.preventDefault();\n  const data = Object.fromEntries(new FormData(form));\n  await fetch(\"\u002Fapi\u002Fprofile\", {\n    method: \"PUT\",\n    headers: { \"Content-Type\": \"application\u002Fjson\" },\n    body: JSON.stringify(data),\n  });\n});","code","js",[],{"id":108,"html":109,"type":84},"b8","\u003Cp>This is a pattern you&#39;ve probably written yourself. \u003Ccode>FormData\u003C\u002Fcode> reads the form&#39;s current values, \u003Ccode>Object.fromEntries\u003C\u002Fcode> turns that into a plain object, \u003Ccode>JSON.stringify\u003C\u002Fcode> turns \u003Cem>that\u003C\u002Fem> into a request body. For a \u003Ccode>name\u003C\u002Fcode> field or an \u003Ccode>email\u003C\u002Fcode> field, it&#39;s exact and correct — the string goes in, the same string comes out the other end.\u003C\u002Fp>",{"id":111,"html":112,"type":84},"b9","\u003Cp>The \u003Ccode>&lt;input type=&quot;file&quot; name=&quot;avatar&quot;&gt;\u003C\u002Fcode> in that same form goes in as a \u003Ccode>File\u003C\u002Fcode> object. And that&#39;s where it quietly falls apart.\u003C\u002Fp>",{"id":114,"html":115,"text":116,"type":97,"level":31},"b10","What \u003Ccode>Object.fromEntries\u003C\u002Fcode> actually hands you","What Object.fromEntries actually hands you",{"id":118,"html":119,"type":84},"b11","\u003Cp>Log \u003Ccode>data.avatar\u003C\u002Fcode> right after that first line and it looks completely normal:\u003C\u002Fp>",{"id":121,"code":122,"type":104,"language":105,"highlight":123},"b12","console.log(data.avatar);\n\u002F\u002F File { name: \"sunset.jpg\", size: 482113, type: \"image\u002Fjpeg\" }",[],{"id":125,"html":126,"type":84},"b13","\u003Cp>Real file. Real name. Real size. Everything about it says &quot;this is fine, carry on.&quot; So you do — straight into \u003Ccode>JSON.stringify\u003C\u002Fcode>.\u003C\u002Fp>",{"id":128,"code":129,"type":104,"language":105,"highlight":130},"b14","console.log(JSON.stringify(data.avatar));\n\u002F\u002F \"{}\"",[],{"id":132,"html":133,"type":84},"b15","\u003Cp>\u003Cstrong>That&#39;s the whole bug, in one line.\u003C\u002Fstrong> Not an error, not \u003Ccode>undefined\u003C\u002Fcode>, not the string \u003Ccode>&quot;[object File]&quot;\u003C\u002Fcode> — an empty JSON object, every single time, for every file, no matter how big or what type. \u003Ccode>JSON.stringify\u003C\u002Fcode> walks an object&#39;s own enumerable properties to build its output. \u003Ccode>File\u003C\u002Fcode> — and \u003Ccode>Blob\u003C\u002Fcode>, which it extends — deliberately doesn&#39;t expose its data that way. \u003Ccode>name\u003C\u002Fcode>, \u003Ccode>size\u003C\u002Fcode>, and \u003Ccode>type\u003C\u002Fcode> are accessors defined on the prototype, not enumerable data sitting on the instance, and the actual bytes aren&#39;t reachable synchronously at all. \u003Ccode>JSON.stringify\u003C\u002Fcode> finds nothing to walk and does exactly what the spec says: it writes out \u003Ccode>{}\u003C\u002Fcode>.\u003C\u002Fp>",{"id":135,"html":136,"type":84},"b16","\u003Cp>So the request that leaves the browser looks like this:\u003C\u002Fp>",{"id":138,"code":139,"type":104,"language":140,"highlight":141},"b17","{ \"displayName\": \"Alex Chen\", \"avatar\": {} }","json",[],{"id":143,"html":144,"type":84},"b18","\u003Cp>The server receives valid JSON, updates the name, sees \u003Ccode>avatar: {}\u003C\u002Fcode>, probably ignores a field it doesn&#39;t recognize the shape of — and returns \u003Ccode>200 OK\u003C\u002Fcode>, because as far as it&#39;s concerned, nothing went wrong. Nothing \u003Cem>did\u003C\u002Fem> go wrong, downstream of the browser. The bug already happened, silently, on your side of the wire.\u003C\u002Fp>",{"id":146,"html":147,"text":148,"type":97,"level":31},"b19","The &quot;fix&quot; that makes it worse","The \"fix\" that makes it worse",{"id":150,"html":151,"type":84},"b20","\u003Cp>The instinct once you find this is: fine, get the file&#39;s actual bytes into the JSON some other way. \u003Ccode>FileReader.readAsDataURL()\u003C\u002Fcode> will happily hand you a base64 string:\u003C\u002Fp>",{"id":153,"code":154,"type":104,"language":105,"highlight":155},"b21","const toBase64 = (file) =>\n  new Promise((resolve) => {\n    const reader = new FileReader();\n    reader.onload = () => resolve(reader.result);\n    reader.readAsDataURL(file);\n  });\n\ndata.avatar = await toBase64(data.avatar); \u002F\u002F now it's a string!\nbody: JSON.stringify(data);",[],{"id":157,"html":158,"type":84},"b22","\u003Cp>This does work — the image genuinely round-trips now. But you&#39;ve traded a silent bug for three quiet costs that show up later instead of immediately:\u003C\u002Fp>",{"id":160,"type":161,"items":162,"ordered":18},"b23","list",[163,164,165],"\u003Cstrong>Base64 inflates the payload by roughly a third.\u003C\u002Fstrong> A 3 MB photo becomes a ~4 MB string, because base64 spends 4 characters to encode every 3 bytes.","\u003Cstrong>The whole file sits in memory twice\u003C\u002Fstrong> — once as the original \u003Ccode>Blob\u003C\u002Fcode>, once as the decoded string — for as long as the request is in flight.","\u003Cstrong>You&#39;ve reinvented \u003Ccode>multipart\u002Fform-data\u003C\u002Fcode>\u003C\u002Fstrong>, badly, using text encoding for something the browser already ships a binary-safe way to send.",{"id":167,"html":168,"type":84},"b24","\u003Cp>None of that fails a test. It just makes uploads slower and heavier in a way nobody notices until someone tries to upload a 20 MB image from their phone.\u003C\u002Fp>",{"id":170,"html":171,"text":171,"type":97,"level":31},"b25","The actual fix: stop converting to JSON",{"id":173,"html":174,"type":84},"b26","\u003Cp>\u003Ccode>FormData\u003C\u002Fcode> was never the problem. Converting it into something else was. \u003Ccode>fetch\u003C\u002Fcode> accepts a \u003Ccode>FormData\u003C\u002Fcode> object as a body directly:\u003C\u002Fp>",{"id":176,"code":177,"type":104,"language":105,"highlight":178},"b27","form.addEventListener(\"submit\", async (e) => {\n  e.preventDefault();\n  const data = new FormData(form); \u002F\u002F don't unwrap it\n  await fetch(\"\u002Fapi\u002Fprofile\", {\n    method: \"PUT\",\n    body: data, \u002F\u002F no headers, no JSON.stringify\n  });\n});",[],{"id":180,"html":181,"type":84},"b28","\u003Cp>Two things to notice, both easy to get backwards:\u003C\u002Fp>",{"id":183,"type":161,"items":184,"ordered":17},"b29",[185,186],"\u003Cstrong>Don&#39;t set \u003Ccode>Content-Type\u003C\u002Fcode> yourself.\u003C\u002Fstrong> \u003Ccode>multipart\u002Fform-data\u003C\u002Fcode> requests need a \u003Ccode>boundary\u003C\u002Fcode> value in the header to separate fields, and \u003Ccode>fetch\u003C\u002Fcode> generates a fresh, unique one for you when it sees a \u003Ccode>FormData\u003C\u002Fcode> body. Set the header manually and you&#39;ll ship it \u003Cem>without\u003C\u002Fem> a boundary, which breaks the request in a way that&#39;s genuinely confusing to debug.","\u003Cstrong>The file&#39;s actual bytes travel this time.\u003C\u002Fstrong> No encoding step, no size penalty, no extra copy in memory — the browser streams the binary data as part of the multipart body, the same mechanism a plain HTML form has used since the 90s.",{"id":188,"html":189,"type":84},"b30","\u003Cp>The one real trade-off: your server needs to parse \u003Ccode>multipart\u002Fform-data\u003C\u002Fcode>, not \u003Ccode>application\u002Fjson\u003C\u002Fcode> — most frameworks have a one-line answer for this already (Express: \u003Ccode>multer\u003C\u002Fcode>; Node&#39;s built-in \u003Ccode>http\u003C\u002Fcode>: \u003Ccode>formidable\u003C\u002Fcode>; plenty of others read it natively).\u003C\u002Fp>",{"id":191,"html":192,"text":193,"type":97,"level":31},"b31","One more thing \u003Ccode>Object.fromEntries\u003C\u002Fcode> was hiding","One more thing Object.fromEntries was hiding",{"id":195,"html":196,"type":84},"b32","\u003Cp>There&#39;s a second gotcha in that first line of code, unrelated to files: \u003Ccode>Object.fromEntries\u003C\u002Fcode> silently drops duplicate keys, keeping only the last one. Add a checkbox group like \u003Ccode>&lt;input type=&quot;checkbox&quot; name=&quot;topics&quot; value=&quot;css&quot;&gt;\u003C\u002Fcode> repeated three times, check two boxes, and \u003Ccode>Object.fromEntries(new FormData(form)).topics\u003C\u002Fcode> gives you exactly one string — not the array you&#39;d expect.\u003C\u002Fp>",{"id":198,"html":199,"type":84},"b33","\u003Cp>\u003Ccode>FormData\u003C\u002Fcode> itself never had this problem. \u003Ccode>formData.getAll(&quot;topics&quot;)\u003C\u002Fcode> returns every checked value, in DOM order, from the start. It&#39;s specifically the trip through \u003Ccode>Object.fromEntries\u003C\u002Fcode> that quietly collapses them — one more reason to hand \u003Ccode>FormData\u003C\u002Fcode> to \u003Ccode>fetch\u003C\u002Fcode> as-is instead of reshaping it first.\u003C\u002Fp>",{"id":201,"html":202,"type":84},"b34","\u003C!-- playground:start -->",{"id":204,"html":205,"text":205,"type":97,"level":31},"b35","🎮 Try it yourself",{"id":207,"html":208,"type":84},"b36","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fformdata-file-upload-json-stringify-trap\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":210,"html":211,"type":84},"b37","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":213,"html":214,"type":84},"b38","\u003C!-- playground:end -->",{"id":216,"html":217,"text":217,"type":97,"level":31},"b39","The lesson",{"id":219,"html":220,"type":84},"b40","\u003Cp>Any time you&#39;re about to call \u003Ccode>JSON.stringify\u003C\u002Fcode> on something that came out of a \u003Ccode>&lt;form&gt;\u003C\u002Fcode>, stop for one second and ask what&#39;s actually in it. Text fields survive the round trip. Files and repeated-name fields don&#39;t — not with an error, just with data quietly missing from the request that already reported success.\u003C\u002Fp>",{"id":222,"html":223,"type":84},"b41","\u003Cp>\u003Ccode>FormData\u003C\u002Fcode> isn&#39;t a stepping stone to JSON. For anything with a file in it, it&#39;s the destination.\u003C\u002Fp>",{"id":225,"html":226,"type":84},"b42","\u003C!-- quiz:start -->",{"id":228,"html":229,"text":229,"type":97,"level":31},"b43","🧠 Test yourself",{"id":231,"html":232,"type":84},"b44","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fformdata-file-upload-json-stringify-trap\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":234,"html":235,"type":84},"b45","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":237,"html":238,"type":84},"b46","\u003C!-- quiz:end -->",{"id":240,"html":241,"type":84},"b47","\u003Cp>Go check your own upload forms — if you see \u003Ccode>Object.fromEntries(new FormData(...))\u003C\u002Fcode> followed anywhere by \u003Ccode>JSON.stringify\u003C\u002Fcode>, that&#39;s worth a five-minute look today. What&#39;s the quietest &quot;it returned 200 but didn&#39;t actually work&quot; bug you&#39;ve shipped?\u003C\u002Fp>",{"id":243,"type":244},"b48","divider",{"id":246,"html":247,"type":84},"b49","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":249,"html":250,"type":84},"b50","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":252,"type":161,"items":253,"ordered":18},"b51",[254,255,256],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","QA signs off on the \"edit profile\" form. Change your display name, hit save, refresh — the new name is there. Ship it.\n\nTwo days later: \"I uploaded a new profile picture and it just... didn't change?\" You check the network tab. The request fired. Status `200 OK`. The server logged a successful update. No red text anywhere. By every signal your tools give you, this worked.\n\nIt didn't. And the bug isn't in the upload handler, the server, or the image itself — it's already dead by the time the request leaves the browser.\n\n**Guess before you scroll:** the file never made it into the request body in the first place. Not corrupted. Not rejected. Just — never there.\n\n## The code that looks completely fine\n\nHere's roughly what shipped:\n\n```js\nform.addEventListener(\"submit\", async (e) => {\n  e.preventDefault();\n  const data = Object.fromEntries(new FormData(form));\n  await fetch(\"\u002Fapi\u002Fprofile\", {\n    method: \"PUT\",\n    headers: { \"Content-Type\": \"application\u002Fjson\" },\n    body: JSON.stringify(data),\n  });\n});\n```\n\nThis is a pattern you've probably written yourself. `FormData` reads the form's current values, `Object.fromEntries` turns that into a plain object, `JSON.stringify` turns *that* into a request body. For a `name` field or an `email` field, it's exact and correct — the string goes in, the same string comes out the other end.\n\nThe `\u003Cinput type=\"file\" name=\"avatar\">` in that same form goes in as a `File` object. And that's where it quietly falls apart.\n\n## What `Object.fromEntries` actually hands you\n\nLog `data.avatar` right after that first line and it looks completely normal:\n\n```js\nconsole.log(data.avatar);\n\u002F\u002F File { name: \"sunset.jpg\", size: 482113, type: \"image\u002Fjpeg\" }\n```\n\nReal file. Real name. Real size. Everything about it says \"this is fine, carry on.\" So you do — straight into `JSON.stringify`.\n\n```js\nconsole.log(JSON.stringify(data.avatar));\n\u002F\u002F \"{}\"\n```\n\n**That's the whole bug, in one line.** Not an error, not `undefined`, not the string `\"[object File]\"` — an empty JSON object, every single time, for every file, no matter how big or what type. `JSON.stringify` walks an object's own enumerable properties to build its output. `File` — and `Blob`, which it extends — deliberately doesn't expose its data that way. `name`, `size`, and `type` are accessors defined on the prototype, not enumerable data sitting on the instance, and the actual bytes aren't reachable synchronously at all. `JSON.stringify` finds nothing to walk and does exactly what the spec says: it writes out `{}`.\n\nSo the request that leaves the browser looks like this:\n\n```json\n{ \"displayName\": \"Alex Chen\", \"avatar\": {} }\n```\n\nThe server receives valid JSON, updates the name, sees `avatar: {}`, probably ignores a field it doesn't recognize the shape of — and returns `200 OK`, because as far as it's concerned, nothing went wrong. Nothing *did* go wrong, downstream of the browser. The bug already happened, silently, on your side of the wire.\n\n## The \"fix\" that makes it worse\n\nThe instinct once you find this is: fine, get the file's actual bytes into the JSON some other way. `FileReader.readAsDataURL()` will happily hand you a base64 string:\n\n```js\nconst toBase64 = (file) =>\n  new Promise((resolve) => {\n    const reader = new FileReader();\n    reader.onload = () => resolve(reader.result);\n    reader.readAsDataURL(file);\n  });\n\ndata.avatar = await toBase64(data.avatar); \u002F\u002F now it's a string!\nbody: JSON.stringify(data);\n```\n\nThis does work — the image genuinely round-trips now. But you've traded a silent bug for three quiet costs that show up later instead of immediately:\n\n- **Base64 inflates the payload by roughly a third.** A 3 MB photo becomes a ~4 MB string, because base64 spends 4 characters to encode every 3 bytes.\n- **The whole file sits in memory twice** — once as the original `Blob`, once as the decoded string — for as long as the request is in flight.\n- **You've reinvented `multipart\u002Fform-data`**, badly, using text encoding for something the browser already ships a binary-safe way to send.\n\nNone of that fails a test. It just makes uploads slower and heavier in a way nobody notices until someone tries to upload a 20 MB image from their phone.\n\n## The actual fix: stop converting to JSON\n\n`FormData` was never the problem. Converting it into something else was. `fetch` accepts a `FormData` object as a body directly:\n\n```js\nform.addEventListener(\"submit\", async (e) => {\n  e.preventDefault();\n  const data = new FormData(form); \u002F\u002F don't unwrap it\n  await fetch(\"\u002Fapi\u002Fprofile\", {\n    method: \"PUT\",\n    body: data, \u002F\u002F no headers, no JSON.stringify\n  });\n});\n```\n\nTwo things to notice, both easy to get backwards:\n\n1. **Don't set `Content-Type` yourself.** `multipart\u002Fform-data` requests need a `boundary` value in the header to separate fields, and `fetch` generates a fresh, unique one for you when it sees a `FormData` body. Set the header manually and you'll ship it *without* a boundary, which breaks the request in a way that's genuinely confusing to debug.\n2. **The file's actual bytes travel this time.** No encoding step, no size penalty, no extra copy in memory — the browser streams the binary data as part of the multipart body, the same mechanism a plain HTML form has used since the 90s.\n\nThe one real trade-off: your server needs to parse `multipart\u002Fform-data`, not `application\u002Fjson` — most frameworks have a one-line answer for this already (Express: `multer`; Node's built-in `http`: `formidable`; plenty of others read it natively).\n\n## One more thing `Object.fromEntries` was hiding\n\nThere's a second gotcha in that first line of code, unrelated to files: `Object.fromEntries` silently drops duplicate keys, keeping only the last one. Add a checkbox group like `\u003Cinput type=\"checkbox\" name=\"topics\" value=\"css\">` repeated three times, check two boxes, and `Object.fromEntries(new FormData(form)).topics` gives you exactly one string — not the array you'd expect.\n\n`FormData` itself never had this problem. `formData.getAll(\"topics\")` returns every checked value, in DOM order, from the start. It's specifically the trip through `Object.fromEntries` that quietly collapses them — one more reason to hand `FormData` to `fetch` as-is instead of reshaping it first.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fformdata-file-upload-json-stringify-trap\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## The lesson\n\nAny time you're about to call `JSON.stringify` on something that came out of a `\u003Cform>`, stop for one second and ask what's actually in it. Text fields survive the round trip. Files and repeated-name fields don't — not with an error, just with data quietly missing from the request that already reported success.\n\n`FormData` isn't a stepping stone to JSON. For anything with a file in it, it's the destination.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fformdata-file-upload-json-stringify-trap\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\nGo check your own upload forms — if you see `Object.fromEntries(new FormData(...))` followed anywhere by `JSON.stringify`, that's worth a five-minute look today. What's the quietest \"it returned 200 but didn't actually work\" bug you've shipped?\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":47,"canonical":259,"description":260},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fformdata-file-upload-json-stringify-trap","An avatar upload form that passes every test, ships to production, returns 200 OK — and never actually uploads the picture. The bug is one function call that silently turns a File ","01a0b426-701c-74eb-b94f-70c9a27741dd",{"id":263,"locked":18},"01a0b426-703f-71b8-a1f9-824891608455",[265],{"id":266,"slug":46,"title":78,"_count":267},"01a0b426-705a-77fc-9c7f-a33c5b694b54",{"questions":268},8,[270],{"locale":13,"slug":46},{"id":266,"slug":46,"title":78,"_count":272,"questionCount":268},{"questions":268},[274,276,278,282,284,288,292,296,300,304,307,308],{"slug":67,"name":68,"articles":275},111,{"slug":61,"name":65,"articles":277},93,{"slug":279,"name":280,"articles":281},"frontend","Frontend",73,{"slug":70,"name":71,"articles":283},39,{"slug":285,"name":286,"articles":287},"css","Css",36,{"slug":289,"name":290,"articles":291},"typescript","Typescript",16,{"slug":293,"name":294,"articles":295},"performance","Performance",14,{"slug":297,"name":298,"articles":299},"react","React",13,{"slug":301,"name":302,"articles":303},"browser","Browser",10,{"slug":305,"name":306,"articles":303},"node","Node",{"slug":73,"name":74,"articles":268},{"slug":309,"name":310,"articles":311},"accessibility","Accessibility",7,{"items":313,"meta":440},[314,332,350,362,381,401,422],{"id":315,"slug":316,"title":317,"subtitle":48,"excerpt":318,"coverUrl":319,"locale":13,"readingMinutes":51,"publishedAt":320,"viewCount":321,"likeCount":19,"commentCount":19,"author":322,"vertical":323,"topic":324,"tags":325,"_count":330,"playground":331,"hasQuiz":17,"hasPlayground":17},"01a0ddae-4ca5-7432-9f7d-559b2267c5dd","permissions-api-query-before-prompt","getCurrentPosition() Doesn't Just Check — It Prompts","You want to peek at whether location access is already granted before showing your own 'why we need this' banner. Calling getCurrentPosition() to find out fires the real system prompt. navigator.permissions.query() reads the state without ever asking.","\u002Fmedia\u002Fcovers\u002Fpermissions-api-query-before-prompt.png","2026-09-26T12:26:10.756Z",62,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[326,327,328,329],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":301,"name":302,"color":48},{"slug":70,"name":71,"color":48},{"assessments":76},{"slug":316},{"id":333,"slug":334,"title":335,"subtitle":48,"excerpt":336,"coverUrl":337,"locale":13,"readingMinutes":51,"publishedAt":338,"viewCount":339,"likeCount":19,"commentCount":19,"author":340,"vertical":341,"topic":342,"tags":343,"_count":348,"playground":349,"hasQuiz":17,"hasPlayground":17},"01a0ddae-453b-76e9-8652-04984e4978ad","indexeddb-localstorage-main-thread-blocking","localStorage Isn't Free — It's Blocking Your Main Thread","Autosave into localStorage looks harmless right up until the draft gets big. The API is synchronous by spec, so every write freezes the page for exactly as long as it takes — and debouncing doesn't fix it. IndexedDB does.","\u002Fmedia\u002Fcovers\u002Findexeddb-localstorage-main-thread-blocking.png","2026-09-26T12:25:07.879Z",49,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[344,345,346,347],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":293,"name":294,"color":48},{"slug":70,"name":71,"color":48},{"assessments":76},{"slug":334},{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":351,"likeCount":19,"commentCount":19,"author":352,"vertical":353,"topic":354,"tags":355,"_count":360,"playground":361,"hasQuiz":17,"hasPlayground":17},46,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[356,357,358,359],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":46},{"id":363,"slug":364,"title":365,"subtitle":48,"excerpt":366,"coverUrl":367,"locale":13,"readingMinutes":368,"publishedAt":369,"viewCount":370,"likeCount":19,"commentCount":19,"author":371,"vertical":372,"topic":373,"tags":374,"_count":379,"playground":380,"hasQuiz":17,"hasPlayground":17},"01a0ddae-3baa-7278-a8e5-48e21a2fe826","requestidlecallback-can-wait-forever","The Background Task That Waited 40 Seconds for 'Idle'","requestIdleCallback promises to run only when the browser has spare cycles — but on a page that's never truly idle, that promise has no upper bound. Here's why the callback can stall, why the timeout escape hatch has its own trap, and when to reach for it instead of setTimeout or scheduler.yield.","\u002Fmedia\u002Fcovers\u002Frequestidlecallback-can-wait-forever.png",6,"2026-09-26T12:24:05.529Z",38,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[375,376,377,378],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":293,"name":294,"color":48},{"slug":279,"name":280,"color":48},{"assessments":76},{"slug":364},{"id":382,"slug":383,"title":384,"subtitle":48,"excerpt":385,"coverUrl":386,"locale":13,"readingMinutes":299,"publishedAt":387,"viewCount":388,"likeCount":19,"commentCount":19,"author":389,"vertical":390,"topic":391,"tags":394,"_count":399,"playground":400,"hasQuiz":17,"hasPlayground":17},"01a066ea-3098-75c9-b7e5-1d5426f773eb","vue-weekly-composables-shared-state-trap","Vue Composables: The Shared State Trap (+ Cheat Sheet)","Why two Vue components using the same composable can end up sharing state by accident, the mental model that explains it, and how to fix or embrace it.","\u002Fmedia\u002Fcovers\u002Fvue-weekly-composables-shared-state-trap.png","2026-09-22T19:37:11.460Z",226,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":392,"name":393},"vue","Vue",[395,396,397,398],{"slug":392,"name":393,"color":48},{"slug":61,"name":65,"color":48},{"slug":70,"name":71,"color":48},{"slug":67,"name":68,"color":48},{"assessments":76},{"slug":383},{"id":402,"slug":403,"title":404,"subtitle":48,"excerpt":405,"coverUrl":406,"locale":13,"readingMinutes":407,"publishedAt":408,"viewCount":409,"likeCount":19,"commentCount":19,"author":410,"vertical":411,"topic":412,"tags":415,"_count":420,"playground":421,"hasQuiz":17,"hasPlayground":17},"01a0b58a-7f56-736a-babf-1796c7b70c19","nextjs-weekly-middleware-to-proxy-network-boundary","Next.js proxy.ts Explained (with Cheat Sheet)","Next.js 16 renamed middleware.ts to proxy.ts and locked it to the Node.js runtime. Learn the network-boundary model, matcher config, and the migration path.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-middleware-to-proxy-network-boundary.png",12,"2026-09-22T19:36:08.416Z",184,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":413,"name":414},"nextjs","Nextjs",[416,417,418,419],{"slug":413,"name":414,"color":48},{"slug":61,"name":65,"color":48},{"slug":70,"name":71,"color":48},{"slug":67,"name":68,"color":48},{"assessments":76},{"slug":403},{"id":423,"slug":424,"title":425,"subtitle":48,"excerpt":426,"coverUrl":427,"locale":13,"readingMinutes":295,"publishedAt":428,"viewCount":429,"likeCount":19,"commentCount":19,"author":430,"vertical":431,"topic":432,"tags":433,"_count":438,"playground":439,"hasQuiz":17,"hasPlayground":17},"01a0ab57-dda2-74dd-816b-a0ef7f77aeb3","react-weekly-derived-state-bug","React Derived State: Why That useState Is Probably a Bug","React derived state explained: why syncing a computed value into useState with an Effect causes extra renders and drift, and what to write instead.","\u002Fmedia\u002Fcovers\u002Freact-weekly-derived-state-bug.png","2026-09-20T16:27:07.271Z",266,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":297,"name":298},[434,435,436,437],{"slug":297,"name":298,"color":48},{"slug":61,"name":65,"color":48},{"slug":70,"name":71,"color":48},{"slug":67,"name":68,"color":48},{"assessments":76},{"slug":424},{"page":76,"perPage":311,"total":277,"totalPages":295},"\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">form\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"submit\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>e\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  e\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">preventDefault\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> data \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Object\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">fromEntries\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FormData\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>form\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">fetch\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"\u002Fapi\u002Fprofile\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    method\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"PUT\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    headers\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \"Content-Type\"\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"application\u002Fjson\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    body\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-num\">JSON\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">stringify\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">console\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">log\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>avatar\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F File { name: \"sunset.jpg\", size: 482113, type: \"image\u002Fjpeg\" }\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">console\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">log\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">JSON\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">stringify\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>avatar\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F \"{}\"\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-json shj-oneline\" data-lang=\"json\">\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\"displayName\"\u003C\u002Fspan>: \u003Cspan class=\"shj-str\">\"Alex Chen\"\u003C\u002Fspan>, \u003Cspan class=\"shj-var\">\"avatar\"\u003C\u002Fspan>: \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-func\">toBase64\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>file\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Promise\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>resolve\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> reader \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FileReader\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    reader\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">onload\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-func\">resolve\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>reader\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>result\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    reader\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">readAsDataURL\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>file\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\ndata\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>avatar \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">toBase64\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>avatar\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F now it's a string!\n\u003C\u002Fspan>body\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-num\">JSON\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">stringify\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">form\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"submit\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>e\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  e\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">preventDefault\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> data \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FormData\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>form\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F don't unwrap it\n\u003C\u002Fspan>  \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">fetch\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"\u002Fapi\u002Fprofile\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    method\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"PUT\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    body\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> data\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-cmnt\">\u002F\u002F no headers, no JSON.stringify\n\u003C\u002Fspan>  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":448},[]]