[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-file-system-access-api-real-file-read-write":44,"search-suggestions":273,"related-file-system-access-api-real-file-read-write":312,"code:js:true:17aa4j5":443,"code:js:true:1bn3ne":444,"code:js:true:eavji1":445,"code:js:true:1rse5li":446,"code:js:true:122moui":447,"comments-01a0ec72-f3f2-7568-9098-75685d817739":448},[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":256,"seo":257,"translationGroupId":260,"series":48,"podcastUrl":48,"verticalId":5,"thread":261,"assessments":263,"translations":269,"quiz":271},"01a0ec72-f3f2-7568-9098-75685d817739","file-system-access-api-real-file-read-write","Your 'Save' Button Makes Copies. The File System Access API Doesn't.",null,"The classic web 'save' — a Blob and an \u003Ca download> link — doesn't overwrite a file, it manufactures a new one every time, leaving notes.md, notes (1).md, notes (2).md scattered in Downloads. The File System Access API fixes this with real, permissioned read-write access to a file on disk. Here's the difference, and why it's Chromium-only progressive enhancement, not a drop-in replacement.","\u002Fmedia\u002Fcovers\u002Ffile-system-access-api-real-file-read-write.png",7,"2026-09-29T12:10:22.219Z",78,{"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},"browser","Browser",{"slug":73,"name":74,"color":48},"frontend","Frontend",{"assessments":76},1,{"slug":46,"title":78},"File System Access API — same file, no duplicates",{"blocks":80,"version":76},[81,85,88,92,95,101,104,107,110,113,116,119,122,125,128,131,135,138,142,145,148,152,155,158,162,165,171,174,177,180,186,189,192,195,199,202,205,208,211,214,217,220,223,226,229,232,238,241,244,247,250],{"id":82,"html":83,"type":84},"b1","\u003Cp>You&#39;re building a little in-browser markdown editor. &quot;Save&quot; is one line: turn the textarea into a Blob, wrap it in an \u003Ccode>&lt;a download&gt;\u003C\u002Fcode>, click it programmatically. Ship it, feels great in your own testing — you save once, done.\u003C\u002Fp>","paragraph",{"id":86,"html":87,"type":84},"b2","\u003Cp>A real user opens \u003Ccode>notes.md\u003C\u002Fcode>, edits it, hits Save. Then edits it again, hits Save again. Then again. They go looking for their notes later and their Downloads folder has \u003Ccode>notes.md\u003C\u002Fcode>, \u003Ccode>notes (1).md\u003C\u002Fcode>, \u003Ccode>notes (2).md\u003C\u002Fcode>, \u003Ccode>notes (3).md\u003C\u002Fcode> — four files, three of them stale, and no indication which one is the one they actually want. They didn&#39;t do anything wrong. Your &quot;Save&quot; button was never a save button. It was a &quot;make a new file&quot; button that happened to reuse a familiar icon.\u003C\u002Fp>",{"id":89,"html":90,"text":90,"type":91,"level":31},"b3","The wrong way, and why it feels right","heading",{"id":93,"html":94,"type":84},"b4","\u003Cp>Here&#39;s the pattern, and it&#39;s everywhere — CodeSandbox-style playgrounds, browser-based note apps, config generators, anything that needs to hand the user a file:\u003C\u002Fp>",{"id":96,"code":97,"type":98,"language":99,"highlight":100},"b5","function saveTheOldWay(text, filename) {\n  const blob = new Blob([text], { type: \"text\u002Fplain\" });\n  const url = URL.createObjectURL(blob);\n  const a = document.createElement(\"a\");\n  a.href = url;\n  a.download = filename;\n  a.click();\n  URL.revokeObjectURL(url);\n}","code","js",[],{"id":102,"html":103,"type":84},"b6","\u003Cp>It works, in the sense that a file lands on disk. But look at what it actually is: \u003Ccode>&lt;a download&gt;\u003C\u002Fcode> triggers the browser&#39;s download pipeline, and the download pipeline has exactly one job — write a new file, and if the name collides, rename it so it doesn&#39;t clobber anything. That collision-avoidance is a deliberate safety feature of downloads in general (you don&#39;t want a sketchy site silently overwriting your existing files), and it&#39;s exactly the behavior you don&#39;t want from a save button. There is no API call in that snippet that means &quot;update the file the user already has open.&quot; There&#39;s no \u003Cem>handle\u003C\u002Fem> to the original file at all — just a one-shot blob with a suggested name.\u003C\u002Fp>",{"id":105,"html":106,"type":84},"b7","\u003Cp>Try it below and watch a simulated Downloads folder fill up — every click is a new file, even though it looks and feels like &quot;saving.&quot;\u003C\u002Fp>",{"id":108,"html":109,"type":84},"b8","\u003C!-- playground:start -->",{"id":111,"html":112,"text":112,"type":91,"level":31},"b9","🎮 Try it yourself",{"id":114,"html":115,"type":84},"b10","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffile-system-access-api-real-file-read-write\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":117,"html":118,"type":84},"b11","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":120,"html":121,"type":84},"b12","\u003C!-- playground:end -->",{"id":123,"html":124,"text":124,"type":91,"level":31},"b13","What was actually missing: a handle",{"id":126,"html":127,"type":84},"b14","\u003Cp>The gap isn&#39;t in your code, it&#39;s in the platform. A regular \u003Ccode>&lt;input type=&quot;file&quot;&gt;\u003C\u002Fcode> gives you a \u003Ccode>File\u003C\u002Fcode> object — bytes, a name, a size — but reading it doesn&#39;t hand you any way to write back to that same path. Browsers spent two decades treating &quot;the filesystem&quot; as something a web page should never be allowed to touch, for good reason: letting arbitrary sites read or overwrite files by name would be catastrophic. So \u003Ccode>&lt;input type=&quot;file&quot;&gt;\u003C\u002Fcode> and \u003Ccode>&lt;a download&gt;\u003C\u002Fcode> were built to avoid ever creating that connection.\u003C\u002Fp>",{"id":129,"html":130,"type":84},"b15","\u003Cp>The \u003Cstrong>File System Access API\u003C\u002Fstrong> is what closes that gap, deliberately and with permission gates at every step. \u003Ccode>window.showOpenFilePicker()\u003C\u002Fcode> doesn&#39;t just return a file&#39;s contents — it returns a \u003Ccode>FileSystemFileHandle\u003C\u002Fcode>, a persistent reference to that specific file that your page can ask to write to later:\u003C\u002Fp>",{"id":132,"code":133,"type":98,"language":99,"highlight":134},"b16","async function openForEditing() {\n  const [handle] = await window.showOpenFilePicker({\n    types: [{ description: \"Markdown\", accept: { \"text\u002Fmarkdown\": [\".md\"] } }],\n  });\n  const file = await handle.getFile();\n  const text = await file.text();\n  return { handle, text };\n}",[],{"id":136,"html":137,"type":84},"b17","\u003Cp>Saving back is where the difference actually shows up. You write through the \u003Cem>same handle\u003C\u002Fem> you opened:\u003C\u002Fp>",{"id":139,"code":140,"type":98,"language":99,"highlight":141},"b18","async function saveToHandle(handle, text) {\n  const writable = await handle.createWritable();\n  await writable.write(text);\n  await writable.close();\n}",[],{"id":143,"html":144,"type":84},"b19","\u003Cp>No new filename, no collision to resolve, no \u003Ccode>(1)\u003C\u002Fcode> appended anywhere. \u003Ccode>createWritable()\u003C\u002Fcode> opens a stream to the exact file the handle points at; \u003Ccode>write()\u003C\u002Fcode> sends the new bytes; \u003Ccode>close()\u003C\u002Fcode> commits them — until then the new bytes go to a temporary file (it starts empty, since \u003Ccode>keepExistingData\u003C\u002Fcode> defaults to \u003Ccode>false\u003C\u002Fcode>), and the original is only replaced when the stream closes. Same file, every time — which is the one property a save button actually needs and the download link structurally cannot provide.\u003C\u002Fp>",{"id":146,"html":147,"type":84},"b20","\u003Cp>That&#39;s not a toy example above — it&#39;s real. Try the second panel in the playground: pick an actual file from your machine, edit it, hit Save (Chrome asks once whether the site may save changes — that&#39;s the permission gate), then check the file on disk. It changed. Same file, same name, no sibling copies.\u003C\u002Fp>",{"id":149,"html":150,"text":151,"type":91,"level":31},"b21","The part that has to stay honest: this is Chromium, not &quot;the web&quot;","The part that has to stay honest: this is Chromium, not \"the web\"",{"id":153,"html":154,"type":84},"b22","\u003Cp>This is the part every &quot;just switch to the File System Access API&quot; take glosses over: \u003Cstrong>Firefox and Safari don&#39;t implement the part that matters here — the file pickers.\u003C\u002Fstrong> They do ship the handle-and-stream half (\u003Ccode>FileSystemFileHandle\u003C\u002Fcode>, \u003Ccode>getFile()\u003C\u002Fcode>, and since Safari 26 in September 2025, \u003Ccode>createWritable()\u003C\u002Fcode>), but only for the sandboxed origin private file system (\u003Ccode>navigator.storage.getDirectory()\u003C\u002Fcode>), which never touches the user&#39;s real files. The half that hands a page an actual file — \u003Ccode>showOpenFilePicker()\u003C\u002Fcode>, \u003Ccode>showSaveFilePicker()\u003C\u002Fcode>, \u003Ccode>showDirectoryPicker()\u003C\u002Fcode> — isn&#39;t &quot;not yet, check back next release&quot;: WebKit&#39;s standards position is &quot;oppose&quot; on security grounds, and Mozilla&#39;s is &quot;negative&quot;. Chrome, Edge, Opera and most other Chromium browsers support the pickers (Brave ships them switched off); that&#39;s the whole list.\u003C\u002Fp>",{"id":156,"html":157,"type":84},"b23","\u003Cp>Feature-detect before you touch it, every time:\u003C\u002Fp>",{"id":159,"code":160,"type":98,"language":99,"highlight":161},"b24","if (\"showOpenFilePicker\" in window) {\n  \u002F\u002F real file handles, real overwrite-in-place\n} else {\n  \u002F\u002F fall back to the Blob + \u003Ca download> approach from above\n}",[],{"id":163,"html":164,"type":84},"b25","\u003Cp>Two more constraints worth knowing before you reach for it, both there on purpose:\u003C\u002Fp>",{"id":166,"type":167,"items":168,"ordered":18},"b26","list",[169,170],"\u003Cstrong>It needs a secure context\u003C\u002Fstrong> (HTTPS or \u003Ccode>localhost\u003C\u002Fcode>) — same rule as most powerful browser APIs.","\u003Cstrong>It needs a real user gesture.\u003C\u002Fstrong> You can&#39;t call \u003Ccode>showOpenFilePicker()\u003C\u002Fcode> on page load or from a timer with no recent click; it needs transient user activation from a click or keypress (and it&#39;s refused outright inside a cross-origin iframe), the same restriction that stops a page from silently spawning a file dialog the moment you land on it.",{"id":172,"html":173,"type":84},"b27","\u003Cp>Neither is a bug to work around — they&#39;re the reason this API was allowed to exist at all. A picker that could open without a click, or run over plain HTTP, would just be the file-access catastrophe browsers spent years avoiding, with better ergonomics.\u003C\u002Fp>",{"id":175,"html":176,"text":176,"type":91,"level":31},"b28","So when is the download link still the right call?",{"id":178,"html":179,"type":84},"b29","\u003Cp>Don&#39;t treat this as &quot;the File System Access API is strictly better, migrate everything.&quot; The download link is still correct when:\u003C\u002Fp>",{"id":181,"type":167,"items":182,"ordered":18},"b30",[183,184,185],"It&#39;s a one-off export that will never be re-saved — a generated report, a chart image, a zip.","You need it to work in Firefox or Safari without a degraded experience — plenty of products can&#39;t accept &quot;20% of visitors get worse behavior.&quot;","The mental model really is &quot;produce a new file,&quot; like exporting a CSV snapshot of current state.",{"id":187,"html":188,"type":84},"b31","\u003Cp>The File System Access API earns its place specifically when the mental model is \u003Cem>edit and re-save an existing file\u003C\u002Fem> — editors, config tools, anything where &quot;Save&quot; should mean &quot;update what&#39;s already there,&quot; not &quot;hand me a fresh copy and I&#39;ll sort out the mess myself.&quot;\u003C\u002Fp>",{"id":190,"html":191,"text":191,"type":91,"level":31},"b32","The fix for the notes app",{"id":193,"html":194,"type":84},"b33","\u003Cp>Back to the markdown editor. The real fix isn&#39;t switching APIs wholesale — it&#39;s keeping a handle around once you have one, and only falling back to the download link when you don&#39;t:\u003C\u002Fp>",{"id":196,"code":197,"type":98,"language":99,"highlight":198},"b34","let currentHandle = null;\n\nasync function save(text) {\n  if (currentHandle) {\n    await saveToHandle(currentHandle, text); \u002F\u002F overwrite in place\n    return;\n  }\n  if (\"showSaveFilePicker\" in window) {\n    currentHandle = await window.showSaveFilePicker({\n      suggestedName: \"notes.md\",\n    });\n    await saveToHandle(currentHandle, text);\n    return;\n  }\n  saveTheOldWay(text, \"notes.md\"); \u002F\u002F Firefox\u002FSafari: still a new file each time\n}",[],{"id":200,"html":201,"type":84},"b35","\u003Cp>First save in a supporting browser prompts once, gets a handle, and keeps it. Every save after that writes to the same file with no dialog and no duplicate. Users on Firefox or Safari get the old, honest-about-its-limits download behavior — worse, but not broken, and not lied to about what &quot;Save&quot; means.\u003C\u002Fp>",{"id":203,"html":204,"type":84},"b36","\u003C!-- quiz:start -->",{"id":206,"html":207,"text":207,"type":91,"level":31},"b37","🧠 Test yourself",{"id":209,"html":210,"type":84},"b38","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffile-system-access-api-real-file-read-write\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":212,"html":213,"type":84},"b39","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":215,"html":216,"type":84},"b40","\u003C!-- quiz:end -->",{"id":218,"html":219,"text":219,"type":91,"level":31},"b41","The takeaway",{"id":221,"html":222,"type":84},"b42","\u003Cp>A &quot;Save&quot; button built on \u003Ccode>&lt;a download&gt;\u003C\u002Fcode> was never overwriting anything — it was generating a new file and letting the browser&#39;s own download manager paper over the difference with a renamed copy. The File System Access API is the first time the platform gives a page an actual handle to write back through, and it comes wrapped in exactly the restrictions you&#39;d want from something that powerful: Chromium-only pickers, secure contexts only, user-gesture only. Feature-detect it, keep the download link as your fallback, and stop calling the old behavior a save.\u003C\u002Fp>",{"id":224,"html":225,"type":84},"b43","\u003Cp>Have you shipped a &quot;save&quot; that was quietly cloning files in someone&#39;s Downloads folder? I&#39;d bet more of us have than will admit it in the comments.\u003C\u002Fp>",{"id":227,"html":228,"type":84},"b44","\u003C!-- related:start -->",{"id":230,"html":231,"text":231,"type":91,"level":31},"b45","📚 Read next",{"id":233,"type":167,"items":234,"ordered":18},"b46",[235,236,237],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fformdata-file-upload-json-stringify-trap\">JSON.stringify Is Quietly Deleting Your File Uploads\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fclipboard-writetext-focus-bug\">The await That Silently Breaks navigator.clipboard.writeText()\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fpermissions-api-query-before-prompt\">getCurrentPosition() Doesn&#39;t Just Check — It Prompts\u003C\u002Fa>",{"id":239,"html":240,"type":84},"b47","\u003C!-- related:end -->",{"id":242,"type":243},"b48","divider",{"id":245,"html":246,"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":248,"html":249,"type":84},"b50","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":251,"type":167,"items":252,"ordered":18},"b51",[253,254,255],"⭐ \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>","You're building a little in-browser markdown editor. \"Save\" is one line: turn the textarea into a Blob, wrap it in an `\u003Ca download>`, click it programmatically. Ship it, feels great in your own testing — you save once, done.\n\nA real user opens `notes.md`, edits it, hits Save. Then edits it again, hits Save again. Then again. They go looking for their notes later and their Downloads folder has `notes.md`, `notes (1).md`, `notes (2).md`, `notes (3).md` — four files, three of them stale, and no indication which one is the one they actually want. They didn't do anything wrong. Your \"Save\" button was never a save button. It was a \"make a new file\" button that happened to reuse a familiar icon.\n\n## The wrong way, and why it feels right\n\nHere's the pattern, and it's everywhere — CodeSandbox-style playgrounds, browser-based note apps, config generators, anything that needs to hand the user a file:\n\n```js\nfunction saveTheOldWay(text, filename) {\n  const blob = new Blob([text], { type: \"text\u002Fplain\" });\n  const url = URL.createObjectURL(blob);\n  const a = document.createElement(\"a\");\n  a.href = url;\n  a.download = filename;\n  a.click();\n  URL.revokeObjectURL(url);\n}\n```\n\nIt works, in the sense that a file lands on disk. But look at what it actually is: `\u003Ca download>` triggers the browser's download pipeline, and the download pipeline has exactly one job — write a new file, and if the name collides, rename it so it doesn't clobber anything. That collision-avoidance is a deliberate safety feature of downloads in general (you don't want a sketchy site silently overwriting your existing files), and it's exactly the behavior you don't want from a save button. There is no API call in that snippet that means \"update the file the user already has open.\" There's no *handle* to the original file at all — just a one-shot blob with a suggested name.\n\nTry it below and watch a simulated Downloads folder fill up — every click is a new file, even though it looks and feels like \"saving.\"\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffile-system-access-api-real-file-read-write\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## What was actually missing: a handle\n\nThe gap isn't in your code, it's in the platform. A regular `\u003Cinput type=\"file\">` gives you a `File` object — bytes, a name, a size — but reading it doesn't hand you any way to write back to that same path. Browsers spent two decades treating \"the filesystem\" as something a web page should never be allowed to touch, for good reason: letting arbitrary sites read or overwrite files by name would be catastrophic. So `\u003Cinput type=\"file\">` and `\u003Ca download>` were built to avoid ever creating that connection.\n\nThe **File System Access API** is what closes that gap, deliberately and with permission gates at every step. `window.showOpenFilePicker()` doesn't just return a file's contents — it returns a `FileSystemFileHandle`, a persistent reference to that specific file that your page can ask to write to later:\n\n```js\nasync function openForEditing() {\n  const [handle] = await window.showOpenFilePicker({\n    types: [{ description: \"Markdown\", accept: { \"text\u002Fmarkdown\": [\".md\"] } }],\n  });\n  const file = await handle.getFile();\n  const text = await file.text();\n  return { handle, text };\n}\n```\n\nSaving back is where the difference actually shows up. You write through the *same handle* you opened:\n\n```js\nasync function saveToHandle(handle, text) {\n  const writable = await handle.createWritable();\n  await writable.write(text);\n  await writable.close();\n}\n```\n\nNo new filename, no collision to resolve, no `(1)` appended anywhere. `createWritable()` opens a stream to the exact file the handle points at; `write()` sends the new bytes; `close()` commits them — until then the new bytes go to a temporary file (it starts empty, since `keepExistingData` defaults to `false`), and the original is only replaced when the stream closes. Same file, every time — which is the one property a save button actually needs and the download link structurally cannot provide.\n\nThat's not a toy example above — it's real. Try the second panel in the playground: pick an actual file from your machine, edit it, hit Save (Chrome asks once whether the site may save changes — that's the permission gate), then check the file on disk. It changed. Same file, same name, no sibling copies.\n\n## The part that has to stay honest: this is Chromium, not \"the web\"\n\nThis is the part every \"just switch to the File System Access API\" take glosses over: **Firefox and Safari don't implement the part that matters here — the file pickers.** They do ship the handle-and-stream half (`FileSystemFileHandle`, `getFile()`, and since Safari 26 in September 2025, `createWritable()`), but only for the sandboxed origin private file system (`navigator.storage.getDirectory()`), which never touches the user's real files. The half that hands a page an actual file — `showOpenFilePicker()`, `showSaveFilePicker()`, `showDirectoryPicker()` — isn't \"not yet, check back next release\": WebKit's standards position is \"oppose\" on security grounds, and Mozilla's is \"negative\". Chrome, Edge, Opera and most other Chromium browsers support the pickers (Brave ships them switched off); that's the whole list.\n\nFeature-detect before you touch it, every time:\n\n```js\nif (\"showOpenFilePicker\" in window) {\n  \u002F\u002F real file handles, real overwrite-in-place\n} else {\n  \u002F\u002F fall back to the Blob + \u003Ca download> approach from above\n}\n```\n\nTwo more constraints worth knowing before you reach for it, both there on purpose:\n\n- **It needs a secure context** (HTTPS or `localhost`) — same rule as most powerful browser APIs.\n- **It needs a real user gesture.** You can't call `showOpenFilePicker()` on page load or from a timer with no recent click; it needs transient user activation from a click or keypress (and it's refused outright inside a cross-origin iframe), the same restriction that stops a page from silently spawning a file dialog the moment you land on it.\n\nNeither is a bug to work around — they're the reason this API was allowed to exist at all. A picker that could open without a click, or run over plain HTTP, would just be the file-access catastrophe browsers spent years avoiding, with better ergonomics.\n\n## So when is the download link still the right call?\n\nDon't treat this as \"the File System Access API is strictly better, migrate everything.\" The download link is still correct when:\n\n- It's a one-off export that will never be re-saved — a generated report, a chart image, a zip.\n- You need it to work in Firefox or Safari without a degraded experience — plenty of products can't accept \"20% of visitors get worse behavior.\"\n- The mental model really is \"produce a new file,\" like exporting a CSV snapshot of current state.\n\nThe File System Access API earns its place specifically when the mental model is *edit and re-save an existing file* — editors, config tools, anything where \"Save\" should mean \"update what's already there,\" not \"hand me a fresh copy and I'll sort out the mess myself.\"\n\n## The fix for the notes app\n\nBack to the markdown editor. The real fix isn't switching APIs wholesale — it's keeping a handle around once you have one, and only falling back to the download link when you don't:\n\n```js\nlet currentHandle = null;\n\nasync function save(text) {\n  if (currentHandle) {\n    await saveToHandle(currentHandle, text); \u002F\u002F overwrite in place\n    return;\n  }\n  if (\"showSaveFilePicker\" in window) {\n    currentHandle = await window.showSaveFilePicker({\n      suggestedName: \"notes.md\",\n    });\n    await saveToHandle(currentHandle, text);\n    return;\n  }\n  saveTheOldWay(text, \"notes.md\"); \u002F\u002F Firefox\u002FSafari: still a new file each time\n}\n```\n\nFirst save in a supporting browser prompts once, gets a handle, and keeps it. Every save after that writes to the same file with no dialog and no duplicate. Users on Firefox or Safari get the old, honest-about-its-limits download behavior — worse, but not broken, and not lied to about what \"Save\" means.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffile-system-access-api-real-file-read-write\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\n## The takeaway\n\nA \"Save\" button built on `\u003Ca download>` was never overwriting anything — it was generating a new file and letting the browser's own download manager paper over the difference with a renamed copy. The File System Access API is the first time the platform gives a page an actual handle to write back through, and it comes wrapped in exactly the restrictions you'd want from something that powerful: Chromium-only pickers, secure contexts only, user-gesture only. Feature-detect it, keep the download link as your fallback, and stop calling the old behavior a save.\n\nHave you shipped a \"save\" that was quietly cloning files in someone's Downloads folder? I'd bet more of us have than will admit it in the comments.\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [JSON.stringify Is Quietly Deleting Your File Uploads](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fformdata-file-upload-json-stringify-trap)\n- [The await That Silently Breaks navigator.clipboard.writeText()](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fclipboard-writetext-focus-bug)\n- [getCurrentPosition() Doesn't Just Check — It Prompts](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fpermissions-api-query-before-prompt)\n\n\u003C!-- related:end -->\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":258,"description":259},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffile-system-access-api-real-file-read-write","The classic web 'save' — a Blob and an \u003Ca download> link — doesn't overwrite a file, it manufactures a new one every time, leaving notes.md, notes (1).md, notes (2).md scattered in","01a0ec72-f3f3-7702-94bc-4d05e2e3235e",{"id":262,"locked":18},"01a0ec72-f7b4-76f4-8095-2d9cd8d5d2c3",[264],{"id":265,"slug":46,"title":266,"_count":267},"01a0ec72-f865-70f7-8914-c0b62acc3aa1","File System Access API: real files, real writes",{"questions":268},8,[270],{"locale":13,"slug":46},{"id":265,"slug":46,"title":266,"_count":272,"questionCount":268},{"questions":268},[274,276,278,280,284,288,292,296,300,302,306,309],{"slug":67,"name":68,"articles":275},114,{"slug":61,"name":65,"articles":277},96,{"slug":73,"name":74,"articles":279},75,{"slug":281,"name":282,"articles":283},"tutorial","Tutorial",41,{"slug":285,"name":286,"articles":287},"css","Css",36,{"slug":289,"name":290,"articles":291},"typescript","Typescript",17,{"slug":293,"name":294,"articles":295},"performance","Performance",14,{"slug":297,"name":298,"articles":299},"react","React",13,{"slug":70,"name":71,"articles":301},11,{"slug":303,"name":304,"articles":305},"node","Node",10,{"slug":307,"name":308,"articles":268},"html","Html",{"slug":310,"name":311,"articles":51},"accessibility","Accessibility",{"items":313,"meta":442},[314,336,348,369,387,405,423],{"id":315,"slug":316,"title":317,"subtitle":48,"excerpt":318,"coverUrl":319,"locale":13,"readingMinutes":299,"publishedAt":320,"viewCount":321,"likeCount":19,"commentCount":19,"author":322,"vertical":323,"topic":324,"tags":327,"_count":334,"playground":335,"hasQuiz":17,"hasPlayground":17},"01a0ec72-faae-767b-a924-5ebb5891d4cd","nuxt-weekly-nitro-server-routes","Nuxt Server Routes Explained: How Nitro Builds Your API","Nuxt server routes turn server\u002Fapi into a real backend via Nitro and h3. Learn routing rules, defineEventHandler, middleware order, and the useState trap.","\u002Fmedia\u002Fcovers\u002Fnuxt-weekly-nitro-server-routes.png","2026-09-29T12:10:53.560Z",65,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":325,"name":326},"nuxt","Nuxt",[328,329,332,333],{"slug":325,"name":326,"color":48},{"slug":330,"name":331,"color":48},"backend","Backend",{"slug":61,"name":65,"color":48},{"slug":281,"name":282,"color":48},{"assessments":76},{"slug":316},{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":337,"likeCount":19,"commentCount":19,"author":338,"vertical":339,"topic":340,"tags":341,"_count":346,"playground":347,"hasQuiz":17,"hasPlayground":17},79,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[342,343,344,345],{"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":349,"slug":350,"title":351,"subtitle":48,"excerpt":352,"coverUrl":353,"locale":13,"readingMinutes":354,"publishedAt":355,"viewCount":356,"likeCount":19,"commentCount":19,"author":357,"vertical":358,"topic":359,"tags":360,"_count":367,"playground":368,"hasQuiz":17,"hasPlayground":17},"01a0ec73-0e91-7572-8a40-07168efe8bbf","scrollend-event-native-scroll-stop-detection","Stop Guessing When Scrolling Stops — Use scrollend","For years, 'has the user stopped scrolling?' was a guess: debounce the scroll event, pick a delay, hope. The scrollend event answers the question directly — and as of Safari 26.2, it finally works everywhere. Here's the bug the guess causes, and the one-line fix.","\u002Fmedia\u002Fcovers\u002Fscrollend-event-native-scroll-stop-detection.png",5,"2026-09-29T09:16:39.108Z",68,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[361,362,363,366],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":364,"name":365,"color":48},"dom","Dom",{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":350},{"id":370,"slug":371,"title":372,"subtitle":48,"excerpt":373,"coverUrl":374,"locale":13,"readingMinutes":354,"publishedAt":375,"viewCount":376,"likeCount":19,"commentCount":19,"author":377,"vertical":378,"topic":379,"tags":380,"_count":385,"playground":386,"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",215,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[381,382,383,384],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":281,"name":282,"color":48},{"assessments":76},{"slug":371},{"id":388,"slug":389,"title":390,"subtitle":48,"excerpt":391,"coverUrl":392,"locale":13,"readingMinutes":354,"publishedAt":393,"viewCount":394,"likeCount":19,"commentCount":19,"author":395,"vertical":396,"topic":397,"tags":398,"_count":403,"playground":404,"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",193,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[399,400,401,402],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":293,"name":294,"color":48},{"slug":281,"name":282,"color":48},{"assessments":76},{"slug":389},{"id":406,"slug":407,"title":408,"subtitle":48,"excerpt":409,"coverUrl":410,"locale":13,"readingMinutes":354,"publishedAt":411,"viewCount":412,"likeCount":19,"commentCount":19,"author":413,"vertical":414,"topic":415,"tags":416,"_count":421,"playground":422,"hasQuiz":17,"hasPlayground":17},"01a0b426-701c-74eb-b94f-6cad6f72a710","formdata-file-upload-json-stringify-trap","JSON.stringify Is Quietly Deleting Your File Uploads","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","2026-09-26T12:24:36.794Z",166,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[417,418,419,420],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":281,"name":282,"color":48},{"slug":307,"name":308,"color":48},{"assessments":76},{"slug":407},{"id":424,"slug":425,"title":426,"subtitle":48,"excerpt":427,"coverUrl":428,"locale":13,"readingMinutes":429,"publishedAt":430,"viewCount":431,"likeCount":19,"commentCount":19,"author":432,"vertical":433,"topic":434,"tags":435,"_count":440,"playground":441,"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",161,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[436,437,438,439],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":293,"name":294,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":425},{"page":76,"perPage":51,"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\">\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">saveTheOldWay\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>text\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> filename\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> blob \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Blob\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>text\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> type\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"text\u002Fplain\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> url \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-num\">URL\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">createObjectURL\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>blob\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> a \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> document\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">createElement\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"a\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  a\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>href \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> url;\n  a\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>download \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> filename;\n  a\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">click\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-num\">URL\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">revokeObjectURL\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>url\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\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\">\u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">openForEditing\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>handle\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> window\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">showOpenFilePicker\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    types\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> description\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"Markdown\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> accept\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \"text\u002Fmarkdown\"\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\".md\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\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-kwd\">const\u003C\u002Fspan> file \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> handle\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">getFile\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> text \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> file\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">text\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> handle\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> text \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n\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>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">saveToHandle\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>handle\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> text\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> writable \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> handle\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">createWritable\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> writable\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">write\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>text\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> writable\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">close\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\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>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"showOpenFilePicker\"\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">in\u003C\u002Fspan> window\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-cmnt\">\u002F\u002F real file handles, real overwrite-in-place\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">else\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-cmnt\">\u002F\u002F fall back to the Blob + &lt;a download&gt; approach from above\n\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>\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>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">let\u003C\u002Fspan> currentHandle \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-num\">null\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">save\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>text\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>currentHandle\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">saveToHandle\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>currentHandle\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> text\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F overwrite in place\n\u003C\u002Fspan>    \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"showSaveFilePicker\"\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">in\u003C\u002Fspan> window\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    currentHandle \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> window\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">showSaveFilePicker\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      suggestedName\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"notes.md\"\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-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">saveToHandle\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>currentHandle\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> text\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-func\">saveTheOldWay\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>text\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"notes.md\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F Firefox\u002FSafari: still a new file each time\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":449},[]]