[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-uint8array-tobase64":44,"search-suggestions":60,"quiz-article-uint8array-tobase64":108,"path-membership-uint8array-tobase64":324},[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,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a18b4e-fc0f-76ec-881a-36bc44cab696","uint8array-tobase64","PRACTICE_QUIZ","btoa() vs toBase64() — do you know why the old way breaks?","Seven questions on why btoa()\u002Fatob() fall over on real data, and what Uint8Array.prototype.toBase64() and Uint8Array.fromBase64() actually do differently.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},7,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,100,104],{"slug":62,"name":63,"articles":64},"webdev","Webdev",168,{"slug":66,"name":67,"articles":68},"javascript","Javascript",135,{"slug":70,"name":71,"articles":72},"frontend","Frontend",94,{"slug":74,"name":75,"articles":76},"tutorial","Tutorial",68,{"slug":78,"name":79,"articles":80},"css","Css",50,{"slug":82,"name":83,"articles":84},"performance","Performance",28,{"slug":86,"name":87,"articles":88},"typescript","Typescript",24,{"slug":90,"name":91,"articles":92},"react","React",20,{"slug":94,"name":95,"articles":96},"browser","Browser",17,{"slug":98,"name":99,"articles":96},"programming","Programming",{"slug":101,"name":102,"articles":103},"node","Node",14,{"slug":105,"name":106,"articles":107},"html","Html",11,{"id":109,"slug":46,"title":110,"subtitle":52,"excerpt":111,"coverUrl":112,"locale":13,"readingMinutes":113,"publishedAt":114,"viewCount":84,"likeCount":19,"commentCount":19,"author":115,"vertical":120,"topic":121,"tags":123,"_count":128,"playground":130,"body":132,"bodyMd":310,"seo":311,"translationGroupId":314,"series":52,"podcastUrl":52,"verticalId":5,"thread":315,"assessments":317,"translations":320,"quiz":322},"01a18b4e-fbba-7593-896a-8808e32d35cc","Stop Hacking Base64 With btoa(). toBase64() Fixes It Natively.","btoa() throws on emoji, and the classic String.fromCharCode(...bytes) workaround crashes on large files. toBase64() and fromBase64() on Uint8Array fix both — no string round trip, no library.","\u002Fmedia\u002Fcovers\u002Fuint8array-tobase64.png",5,"2026-11-05T06:10:25.995Z",{"id":116,"name":117,"username":118,"avatarUrl":52,"headline":119},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":66,"name":122},"JavaScript",[124,125,126,127],{"slug":66,"name":67,"color":52},{"slug":62,"name":63,"color":52},{"slug":94,"name":95,"color":52},{"slug":74,"name":75,"color":52},{"assessments":129},1,{"slug":46,"title":131},"btoa() vs toBase64() — interactive playground",{"blocks":133,"version":129},[134,138,141,147,150,155,160,163,166,169,172,175,179,182,185,189,192,195,198,201,204,207,210,213,216,219,222,226,229,233,236,240,243,247,250,254,257,260,263,267,270,273,276,279,282,285,288,291,294,297,300,303],{"id":135,"html":136,"type":137},"b1","\u003Cp>The feature was simple: let someone export a report and share it as a single link, PDF baked right in as a data URL. Worked first try. Worked in the demo. Worked for two weeks in production.\u003C\u002Fp>","paragraph",{"id":139,"html":140,"type":137},"b2","\u003Cp>Then someone exported a 40-page report, and the share button threw:\u003C\u002Fp>",{"id":142,"code":143,"type":144,"language":145,"highlight":146},"b3","RangeError: Maximum call stack size exceeded","code","plain",[],{"id":148,"html":149,"type":137},"b4","\u003Cp>Not a network error. Not a permissions error. The encoding step itself — the part of the code nobody had touched since the first version — blew the call stack on a PDF that was, by file standards, perfectly ordinary.\u003C\u002Fp>",{"id":151,"html":152,"text":153,"type":154,"level":31},"b5","The code that worked right up until it didn&#39;t","The code that worked right up until it didn't","heading",{"id":156,"code":157,"type":144,"language":158,"highlight":159},"b6","async function shareReport(url) {\n  const res = await fetch(url);\n  const bytes = new Uint8Array(await res.arrayBuffer());\n  const base64 = btoa(String.fromCharCode(...bytes));\n  return `data:application\u002Fpdf;base64,${base64}`;\n}","js",[],{"id":161,"html":162,"type":137},"b7","\u003Cp>This is the pattern you&#39;ll find in half the Stack Overflow answers for &quot;convert ArrayBuffer to base64 JavaScript.&quot; It reads fine. It passed code review. It&#39;s also a time bomb, and the fuse length depends on file size.\u003C\u002Fp>",{"id":164,"html":165,"type":137},"b8","\u003Cp>\u003Cstrong>Before you keep reading: what do you think \u003Ccode>...bytes\u003C\u002Fcode> is actually doing to \u003Ccode>String.fromCharCode\u003C\u002Fcode>?\u003C\u002Fstrong>\u003C\u002Fp>",{"id":167,"html":168,"type":137},"b9","\u003Cp>It&#39;s not passing an array. \u003Ccode>String.fromCharCode\u003C\u002Fcode> doesn&#39;t take one — it takes a list of arguments, one per character code. The spread operator expands every single byte into its own argument in that function call. A 2KB test file is 2,000-ish arguments. Fine. A 40-page PDF is tens of thousands of bytes, which means tens of thousands of arguments in one call — and every JS engine caps how many arguments a function call can take. Cross that line and you don&#39;t get a graceful error, you get a stack blow-up with a message that tells you nothing about where it came from.\u003C\u002Fp>",{"id":170,"html":171,"text":171,"type":154,"level":31},"b10","It gets worse with text, not just files",{"id":173,"html":174,"type":137},"b11","\u003Cp>Swap the binary file for something simpler — base64-encoding a string for a shareable URL — and \u003Ccode>btoa()\u003C\u002Fcode> has a \u003Cem>second\u003C\u002Fem> failure mode:\u003C\u002Fp>",{"id":176,"code":177,"type":144,"language":158,"highlight":178},"b12","btoa(\"Hello 👋 World\");\n\u002F\u002F DOMException: Failed to execute 'btoa' on 'Window':\n\u002F\u002F The string to be encoded contains characters outside of\n\u002F\u002F the Latin1 range.",[],{"id":180,"html":181,"type":137},"b13","\u003Cp>\u003Ccode>btoa()\u003C\u002Fcode> was never built to handle arbitrary text. Its actual contract is a &quot;byte string&quot; — every character has to map to a single byte, U+0000 through U+00FF. That range covers plenty of accented Latin letters (\u003Ccode>café\u003C\u002Fcode> and \u003Ccode>José\u003C\u002Fcode> actually encode fine, just not as UTF-8), but step outside it — an emoji, an em dash, a name with a character like \u003Ccode>Ł\u003C\u002Fcode> or a Cyrillic letter in it — and it&#39;s an instant throw.\u003C\u002Fp>",{"id":183,"html":184,"type":137},"b14","\u003Cp>The classic fix you&#39;ll find for this one is uglier than the bug:\u003C\u002Fp>",{"id":186,"code":187,"type":144,"language":158,"highlight":188},"b15","btoa(unescape(encodeURIComponent(text)));",[],{"id":190,"html":191,"type":137},"b16","\u003Cp>It works, in the sense that it doesn&#39;t throw. It also routes your string through \u003Ccode>unescape()\u003C\u002Fcode> — a function the spec deprecated decades ago and keeps around purely so this exact hack doesn&#39;t break the web. If you&#39;ve ever wondered why base64-encoding a string in JavaScript feels like three functions wearing a trenchcoat, this is why: \u003Ccode>btoa()\u003C\u002Fcode> does one narrow job, and everything else is a workaround bolted on from outside.\u003C\u002Fp>",{"id":193,"html":194,"text":194,"type":154,"level":31},"b17","Why both bugs have the same root cause",{"id":196,"html":197,"type":137},"b18","\u003Cp>\u003Ccode>btoa()\u003C\u002Fcode> and \u003Ccode>atob()\u003C\u002Fcode> operate on strings, not bytes. Any time your actual data is binary — a fetched file, a crypto digest, Unicode text — you have to convert it into a byte-string-shaped JavaScript string first. That conversion step is where both bugs live: \u003Ccode>String.fromCharCode(...bytes)\u003C\u002Fcode> is how you build that string for binary data, and it&#39;s the one that blows the call stack. \u003Ccode>encodeURIComponent\u003C\u002Fcode> plus \u003Ccode>unescape\u003C\u002Fcode> is how you build it for Unicode text, and it&#39;s the one that&#39;s a deprecated-function sandwich.\u003C\u002Fp>",{"id":199,"html":200,"type":137},"b19","\u003Cp>The actual problem was never base64. It was the round trip through a string that base64 never needed in the first place.\u003C\u002Fp>",{"id":202,"html":203,"type":137},"b20","\u003C!-- playground:start -->",{"id":205,"html":206,"text":206,"type":154,"level":31},"b21","🎮 Try it yourself",{"id":208,"html":209,"type":137},"b22","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fuint8array-tobase64\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":211,"html":212,"type":137},"b23","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":214,"html":215,"type":137},"b24","\u003C!-- playground:end -->",{"id":217,"html":218,"text":218,"type":154,"level":31},"b25","The fix: encode the bytes directly",{"id":220,"html":221,"type":137},"b26","\u003Cp>As of late 2025, \u003Ccode>Uint8Array\u003C\u002Fcode> has two methods that skip the string entirely:\u003C\u002Fp>",{"id":223,"code":224,"type":144,"language":158,"highlight":225},"b27","async function shareReport(url) {\n  const res = await fetch(url);\n  const bytes = new Uint8Array(await res.arrayBuffer());\n  const base64 = bytes.toBase64(); \u002F\u002F no spread, no stack limit\n  return `data:application\u002Fpdf;base64,${base64}`;\n}",[],{"id":227,"html":228,"type":137},"b28","\u003Cp>One call. No \u003Ccode>String.fromCharCode\u003C\u002Fcode>, no spread, no argument-count ceiling, because nothing is being passed as individual arguments — \u003Ccode>toBase64()\u003C\u002Fcode> walks the bytes internally and hands back a string. Decoding mirrors it exactly:\u003C\u002Fp>",{"id":230,"code":231,"type":144,"language":158,"highlight":232},"b29","const bytes = Uint8Array.fromBase64(base64);",[],{"id":234,"html":235,"type":137},"b30","\u003Cp>For the text case, pair it with \u003Ccode>TextEncoder\u003C\u002Fcode>\u002F\u003Ccode>TextDecoder\u003C\u002Fcode> instead of the \u003Ccode>unescape\u003C\u002Fcode> hack:\u003C\u002Fp>",{"id":237,"code":238,"type":144,"language":158,"highlight":239},"b31","const base64 = new TextEncoder().encode(\"Hello 👋 World\").toBase64();\nconst text = new TextDecoder().decode(Uint8Array.fromBase64(base64));\n\u002F\u002F text === \"Hello 👋 World\"",[],{"id":241,"html":242,"type":137},"b32","\u003Cp>\u003Ccode>TextEncoder\u003C\u002Fcode> turns the string into UTF-8 bytes correctly — emoji, accents, anything — and \u003Ccode>toBase64()\u003C\u002Fcode> takes it from there. No deprecated functions, no silent mis-encoding.\u003C\u002Fp>",{"id":244,"html":245,"text":246,"type":154,"level":43},"b33","The option you&#39;ll actually reach for: URL-safe tokens","The option you'll actually reach for: URL-safe tokens",{"id":248,"html":249,"type":137},"b34","\u003Cp>Standard base64 uses \u003Ccode>+\u003C\u002Fcode> and \u003Ccode>\u002F\u003C\u002Fcode>, both of which need escaping in a URL or a filename. \u003Ccode>toBase64()\u003C\u002Fcode> takes an \u003Ccode>alphabet\u003C\u002Fcode> option so you don&#39;t hand-roll the replacement:\u003C\u002Fp>",{"id":251,"code":252,"type":144,"language":158,"highlight":253},"b35","const token = crypto.getRandomValues(new Uint8Array(16))\n  .toBase64({ alphabet: \"base64url\", omitPadding: true });\n\u002F\u002F \"Kx8pQ2v9mZt3dF7h...\"  — safe to drop straight into a URL, no `+`, `\u002F`, or `=`",[],{"id":255,"html":256,"type":137},"b36","\u003Cp>\u003Ccode>omitPadding: true\u003C\u002Fcode> strips the trailing \u003Ccode>=\u003C\u002Fcode> characters too — the part that trips up anyone who&#39;s ever copy-pasted a base64 token into a query string and watched it get mangled.\u003C\u002Fp>",{"id":258,"html":259,"text":259,"type":154,"level":31},"b37","The part worth being honest about",{"id":261,"html":262,"type":137},"b38","\u003Cp>\u003Ccode>Uint8Array.prototype.toBase64()\u003C\u002Fcode> and \u003Ccode>Uint8Array.fromBase64()\u003C\u002Fcode> reached Baseline &quot;newly available&quot; in September 2025 — Chrome 140, Firefox 133, Safari 18.2, and the versions after. That&#39;s all three engines, not a single-vendor experiment, which is why it&#39;s worth adopting now rather than waiting. It is \u003Cem>not\u003C\u002Fem> in anything older than that, though, so if your app still supports last year&#39;s browsers, feature-detect before you rely on it:\u003C\u002Fp>",{"id":264,"code":265,"type":144,"language":158,"highlight":266},"b39","const hasNativeBase64 = typeof Uint8Array.prototype.toBase64 === \"function\";",[],{"id":268,"html":269,"type":137},"b40","\u003Cp>Keep your \u003Ccode>btoa\u003C\u002Fcode>\u002F\u003Ccode>atob\u003C\u002Fcode> path behind that check for a while, rather than ripping it out the day you read this.\u003C\u002Fp>",{"id":271,"html":272,"text":272,"type":154,"level":31},"b41","The report export, fixed",{"id":274,"html":275,"type":137},"b42","\u003Cp>The 40-page PDF that used to crash the share button now encodes in one line, with no upper bound tied to how many arguments a function call can hold. The emoji that used to throw a \u003Ccode>DOMException\u003C\u002Fcode> round-trips cleanly through \u003Ccode>TextEncoder\u003C\u002Fcode>. Neither fix required a library — both were already shipping in the runtime, waiting for the hand-rolled version to get retired.\u003C\u002Fp>",{"id":277,"html":278,"type":137},"b43","\u003Cp>If you&#39;ve still got a \u003Ccode>String.fromCharCode(...bytes)\u003C\u002Fcode> or an \u003Ccode>unescape(encodeURIComponent(...))\u003C\u002Fcode> somewhere in your codebase, it&#39;s probably not broken yet. It&#39;s just waiting for a big enough file or a weird enough character. What&#39;s the oldest base64 hack still running in your production code — do you know what size file would finally break it?\u003C\u002Fp>",{"id":280,"html":281,"type":137},"b44","\u003C!-- quiz:start -->",{"id":283,"html":284,"text":284,"type":154,"level":31},"b45","🧠 Test yourself",{"id":286,"html":287,"type":137},"b46","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fuint8array-tobase64\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":289,"html":290,"type":137},"b47","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":292,"html":293,"type":137},"b48","\u003C!-- quiz:end -->",{"id":295,"type":296},"b49","divider",{"id":298,"html":299,"type":137},"b50","\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":301,"html":302,"type":137},"b51","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":304,"type":305,"items":306,"ordered":18},"b52","list",[307,308,309],"⭐ \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>","The feature was simple: let someone export a report and share it as a single link, PDF baked right in as a data URL. Worked first try. Worked in the demo. Worked for two weeks in production.\n\nThen someone exported a 40-page report, and the share button threw:\n\n```\nRangeError: Maximum call stack size exceeded\n```\n\nNot a network error. Not a permissions error. The encoding step itself — the part of the code nobody had touched since the first version — blew the call stack on a PDF that was, by file standards, perfectly ordinary.\n\n## The code that worked right up until it didn't\n\n```js\nasync function shareReport(url) {\n  const res = await fetch(url);\n  const bytes = new Uint8Array(await res.arrayBuffer());\n  const base64 = btoa(String.fromCharCode(...bytes));\n  return `data:application\u002Fpdf;base64,${base64}`;\n}\n```\n\nThis is the pattern you'll find in half the Stack Overflow answers for \"convert ArrayBuffer to base64 JavaScript.\" It reads fine. It passed code review. It's also a time bomb, and the fuse length depends on file size.\n\n**Before you keep reading: what do you think `...bytes` is actually doing to `String.fromCharCode`?**\n\nIt's not passing an array. `String.fromCharCode` doesn't take one — it takes a list of arguments, one per character code. The spread operator expands every single byte into its own argument in that function call. A 2KB test file is 2,000-ish arguments. Fine. A 40-page PDF is tens of thousands of bytes, which means tens of thousands of arguments in one call — and every JS engine caps how many arguments a function call can take. Cross that line and you don't get a graceful error, you get a stack blow-up with a message that tells you nothing about where it came from.\n\n## It gets worse with text, not just files\n\nSwap the binary file for something simpler — base64-encoding a string for a shareable URL — and `btoa()` has a *second* failure mode:\n\n```js\nbtoa(\"Hello 👋 World\");\n\u002F\u002F DOMException: Failed to execute 'btoa' on 'Window':\n\u002F\u002F The string to be encoded contains characters outside of\n\u002F\u002F the Latin1 range.\n```\n\n`btoa()` was never built to handle arbitrary text. Its actual contract is a \"byte string\" — every character has to map to a single byte, U+0000 through U+00FF. That range covers plenty of accented Latin letters (`café` and `José` actually encode fine, just not as UTF-8), but step outside it — an emoji, an em dash, a name with a character like `Ł` or a Cyrillic letter in it — and it's an instant throw.\n\nThe classic fix you'll find for this one is uglier than the bug:\n\n```js\nbtoa(unescape(encodeURIComponent(text)));\n```\n\nIt works, in the sense that it doesn't throw. It also routes your string through `unescape()` — a function the spec deprecated decades ago and keeps around purely so this exact hack doesn't break the web. If you've ever wondered why base64-encoding a string in JavaScript feels like three functions wearing a trenchcoat, this is why: `btoa()` does one narrow job, and everything else is a workaround bolted on from outside.\n\n## Why both bugs have the same root cause\n\n`btoa()` and `atob()` operate on strings, not bytes. Any time your actual data is binary — a fetched file, a crypto digest, Unicode text — you have to convert it into a byte-string-shaped JavaScript string first. That conversion step is where both bugs live: `String.fromCharCode(...bytes)` is how you build that string for binary data, and it's the one that blows the call stack. `encodeURIComponent` plus `unescape` is how you build it for Unicode text, and it's the one that's a deprecated-function sandwich.\n\nThe actual problem was never base64. It was the round trip through a string that base64 never needed in the first place.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fuint8array-tobase64\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 fix: encode the bytes directly\n\nAs of late 2025, `Uint8Array` has two methods that skip the string entirely:\n\n```js\nasync function shareReport(url) {\n  const res = await fetch(url);\n  const bytes = new Uint8Array(await res.arrayBuffer());\n  const base64 = bytes.toBase64(); \u002F\u002F no spread, no stack limit\n  return `data:application\u002Fpdf;base64,${base64}`;\n}\n```\n\nOne call. No `String.fromCharCode`, no spread, no argument-count ceiling, because nothing is being passed as individual arguments — `toBase64()` walks the bytes internally and hands back a string. Decoding mirrors it exactly:\n\n```js\nconst bytes = Uint8Array.fromBase64(base64);\n```\n\nFor the text case, pair it with `TextEncoder`\u002F`TextDecoder` instead of the `unescape` hack:\n\n```js\nconst base64 = new TextEncoder().encode(\"Hello 👋 World\").toBase64();\nconst text = new TextDecoder().decode(Uint8Array.fromBase64(base64));\n\u002F\u002F text === \"Hello 👋 World\"\n```\n\n`TextEncoder` turns the string into UTF-8 bytes correctly — emoji, accents, anything — and `toBase64()` takes it from there. No deprecated functions, no silent mis-encoding.\n\n### The option you'll actually reach for: URL-safe tokens\n\nStandard base64 uses `+` and `\u002F`, both of which need escaping in a URL or a filename. `toBase64()` takes an `alphabet` option so you don't hand-roll the replacement:\n\n```js\nconst token = crypto.getRandomValues(new Uint8Array(16))\n  .toBase64({ alphabet: \"base64url\", omitPadding: true });\n\u002F\u002F \"Kx8pQ2v9mZt3dF7h...\"  — safe to drop straight into a URL, no `+`, `\u002F`, or `=`\n```\n\n`omitPadding: true` strips the trailing `=` characters too — the part that trips up anyone who's ever copy-pasted a base64 token into a query string and watched it get mangled.\n\n## The part worth being honest about\n\n`Uint8Array.prototype.toBase64()` and `Uint8Array.fromBase64()` reached Baseline \"newly available\" in September 2025 — Chrome 140, Firefox 133, Safari 18.2, and the versions after. That's all three engines, not a single-vendor experiment, which is why it's worth adopting now rather than waiting. It is *not* in anything older than that, though, so if your app still supports last year's browsers, feature-detect before you rely on it:\n\n```js\nconst hasNativeBase64 = typeof Uint8Array.prototype.toBase64 === \"function\";\n```\n\nKeep your `btoa`\u002F`atob` path behind that check for a while, rather than ripping it out the day you read this.\n\n## The report export, fixed\n\nThe 40-page PDF that used to crash the share button now encodes in one line, with no upper bound tied to how many arguments a function call can hold. The emoji that used to throw a `DOMException` round-trips cleanly through `TextEncoder`. Neither fix required a library — both were already shipping in the runtime, waiting for the hand-rolled version to get retired.\n\nIf you've still got a `String.fromCharCode(...bytes)` or an `unescape(encodeURIComponent(...))` somewhere in your codebase, it's probably not broken yet. It's just waiting for a big enough file or a weird enough character. What's the oldest base64 hack still running in your production code — do you know what size file would finally break it?\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fuint8array-tobase64\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---\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":110,"canonical":312,"description":313},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fuint8array-tobase64","btoa() throws on emoji, and the classic String.fromCharCode(...bytes) workaround crashes on large files. toBase64() and fromBase64() on Uint8Array fix both — no string round trip, ","01a18b4e-fbba-7593-896a-8f9181d0677c",{"id":316,"locked":18},"01a18b4e-fbea-73fc-bc4a-f6733be7fc02",[318],{"id":45,"slug":46,"title":48,"_count":319},{"questions":51},[321],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":323,"questionCount":51},{"questions":51},[]]