Hoe converteer je tekst naar Base64 in JavaScript (en decodeer het)
De one-liner die werkt, degene die een fout geeft, en waarom — plus de UTF-8-veilige roundtrip, de native methoden van 2025 en de verschillen in Node.js.
Om een string Base64-gecodeerd te maken in JavaScript, roep je btoa('Hello, world!') aan, wat 'SGVsbG8sIHdvcmxkIQ==' retourneert. Deze one-liner werkt alleen voor ASCII- en Latin-1-tekst: zodra je een emoji, een accent of een Chinees karakter doorgeeft, gooit hij InvalidCharacterError. Deze gids toont waarom dit gebeurt en de UTF-8-veilige roundtrip die het oplost, inclusief de nieuwe native methoden van 2025.
Base64 in één regel: btoa() en atob()
Het snelste antwoord: encodeer met btoa('Hello, world!'), decodeer met atob('SGVsbG8sIHdvcmxkIQ=='). Beide geverifieerd: btoa('a') retourneert 'YQ==', en atob('SA==') retourneert 'S'.
Wat btoa() eigenlijk doet, is je string behandelen als een "binary string", een string waarin elk teken staat voor precies één byte binaire data, en die bytes vervolgens Base64-encoderen. Het MDN-usecase is data die transmissieproblemen veroorzaakt, zoals ASCII-besturingskarakters (0–31), die je door een tekstkanaal wilt smokkelen.
Deze one-liner is voldoende als je tekst ASCII is: het codepunt van elk teken moet onder de 256 liggen, en volgens MDN geldt de byte-naar-codepunt-mapping alleen betrouwbaar tot 0x7F. Het bereik 0x80–0xFF werkt technisch gezien wel, maar het vertrouwt op Latin-1-encodinggedrag dat niet draagbaar is over omgevingen heen. Een goede vuistregel: alles wat niet-ASCII is, volgt het UTF-8-pad hieronder.
Waarom btoa() InvalidCharacterError gooit op emoji, accenten en CJK
Base64 is een binaire encoding, geen tekstencoding. MDN's glossarium noemt de gewoonte om willekeurige tekst of HTML aan btoa() te geven een veelvoorkomend misverstand: btoa/atob zijn ouder dan getypte arrays, dus ze gebruiken strings om bytes voor te stellen. Het probleem begint wanneer je string niet één byte per teken is.
JavaScript-strings zijn UTF-16, dus een code-eenheid is altijd twee bytes, en tekens buiten het BMP (zoals emoji) nemen vier bytes in beslag als surrogate pair; UTF-8 gebruikt één tot vier bytes per codepoint. btoa() en atob() werken alleen met single-byte (Latin-1-bereik) karakters. Probeer iets breder te encoderen en je krijgt deze exacte fout:
DOMException: Failed to execute 'btoa' on 'Window':
The string to be encoded contains characters outside of the Latin1 range
Het brekende geval is triviaal te reproduceren: btoa('✓') gooit een fout, en btoa('a Ā 𐀀 文 🦄') doet hetzelfde. Emoji, letters met accenten buiten Latin-1 en CJK-karakters zitten allemaal boven 0xFF, dus geen van ze past binnen de aanname van één byte per teken. '✓' is U+2713, ver boven 0xFF, dus btoa() weigert in plaats van je data stilzwijgend te corrupteren. Die weigering is misschien wel de beste eigenschap van de API: hij faalt luidruchtig in plaats van mojibake te produceren die je weken later ontdekt.
Nog een ding dat de moeite waard is om te weten voordat we dit oplossen: Base64 maakt data groter, met ongeveer 33% ten opzichte van de ruwe bytes. Dat is prima voor korte strings, tokens en data-URI's, maar een echte reden om grote payloads niet te Base64-encoden — encodeer bij de grens van je systeem waar je het nodig hebt, niet overal.

Het edge case van de lone surrogate (en String.isWellFormed())
De meeste artikelen slaan dit over, maar het kan pijn doen: een UTF-16-string die een lone surrogate bevat (de helft van een emoji-paar, meestal door een slicing-bug) is "malformed". Volgens web.dev's gids substitueert TextDecoder standaard stilzwijgend U+FFFD. Controleer vóór het encoderen de welgevormdheid met String.prototype.isWellFormed(), of vang de URIError af die encodeURIComponent() gooit bij lone surrogates, en besluit dan expliciet: wijs de data af, schakel vervanging bewust in, of gooi een fout voor latere analyse.
De juiste manier: TextEncoder → UTF-8-bytes → Base64
De juiste oplossing is om je string eerst met TextEncoder naar UTF-8-bytes te converteren, en daarna de bytes Base64-gecodeerd te maken in plaats van de string. new TextEncoder().encode(str) zet elke JavaScript-string om in een Uint8Array van UTF-8-bytes (TextEncoder spreekt alleen UTF-8). Op de terugweg gebruikt new TextDecoder() standaard UTF-8 en zet het exact terug. Let op wat atob() je hier geeft: een binary string, één karakter per byte, geen UTF-8-tekst. Daarom heeft de decodeerkant TextDecoder nodig om bytes terug te zetten in leesbare tekst.
bytesToBase64() en base64ToBytes() helpers (copy-paste code)
Hier is MDN's canonieke roundtrip, die in elke browser werkt die TextEncoder ondersteunt (IE uitgezonderd):
function bytesToBase64(bytes) {
const binString = Array.from(bytes, (b) => String.fromCodePoint(b)).join('');
return btoa(binString);
}
function base64ToBytes(base64) {
const binString = atob(base64);
return Uint8Array.from(binString, (m) => m.codePointAt(0));
}
Verifieer met een volledige roundtrip
Een string encoderen wordt één regel:
const encoded = bytesToBase64(new TextEncoder().encode('a Ā 𐀀 文 🦄'));
// 'YSDEgCDwkICAIOaWhyDwn6aE'
Decoderen is het omgekeerde:
const decoded = new TextDecoder().decode(base64ToBytes(encoded));
// 'a Ā 𐀀 文 🦄'
web.dev's geverifieerde roundtrip bevestigt hetzelfde patroon: 'hello⛳❤️🧀' encodeert naar 'aGVsbG/im7PinaTvuI/wn6eA' en decodeert exact terug, inclusief emoji.
Sla de unescape(encodeURIComponent())-truc over
Je vindt btoa(unescape(encodeURIComponent(str))) overal gekopieerd. Het werkt, maar unescape() is Annex B-legacy, verouderd sinds ECMAScript 3. Nieuwe code zou TextEncoder moeten gebruiken: het is expliciet, standaard en hangt niet af van een verouderde global.
De manier van 2025: Uint8Array.toBase64() en Uint8Array.fromBase64()
Sinds september 2025 heeft JavaScript native Base64-methoden op Uint8Array zelf, dus de byte-string-helpers hierboven zijn niet langer de enige optie. Het TC39-Base64-voorstel heeft Stage 4 bereikt en maakt deel uit van de taal; het TC39-argument is dat base64 geen tekstencoding is, dus zijn TextEncoder/TextDecoder conceptueel het verkeerde gereedschap ervoor.
Volgens MDN zijn dit Baseline "Newly available" in alle moderne browsers. Controleer je doelomgevingen voordat je code uitrolt: als je oudere browsers of oudere Node-versies ondersteunt, behoud dan de TextEncoder + btoa fallback. MDN geeft de voorkeur aan deze methoden boven btoa()/atob() wanneer je al een Uint8Array hebt, omdat er geen ruwe byte-string hoeft te worden aangemaakt.
Encoderen: bytes.toBase64()
const b64 = new TextEncoder().encode('Hello, world!').toBase64();
// 'SGVsbG8sIHdvcmxkIQ=='
Eén UTF-8-conversie, één methode-aanroep, klaar.
Decoderen: Uint8Array.fromBase64()
const text = new TextDecoder().decode(Uint8Array.fromBase64(b64));
// 'Hello, world!'
MDN merkt op dat fromBase64() de voorkeur verdient boven atob() omdat het een echte byte-array retourneert, tenzij de gedecodeerde data bedoeld is als ASCII-tekst. Het accepteert alleen het base64-alphabet (A–Z, a–z, 0–9, plus +/ of -_), optionele = padding, en negeert ASCII-witruimte; het gooit een SyntaxError bij tekens buiten het alphabet.
Opties: alphabet, omitPadding, lastChunkHandling
De opties van de specificatie geeft je precieze controle:
| Optie | Waarden | Wat het doet |
|---|---|---|
alphabet |
'base64' (standaard), 'base64url' |
+// vs -/_ |
omitPadding |
boolean | Verwijdert trailing =: new Uint8Array([72]).toBase64() → 'SA==', met omitPadding → 'SA' |
lastChunkHandling |
'loose' (standaard), 'strict', 'stop-before-partial' |
Decoder-tolerantie voor het laatste chunk |
'loose' tolereert ontbrekende padding en witruimte. 'strict' vereist exacte chunks van 4 tekens met overflow-bits die op nul gezet zijn, wat je wilt bij het valideren van onbetrouwbare input. 'stop-before-partial' is voor streaming. De standaard losse modus tolereert ook overflow-bits die niet op nul gezet zijn; de strikte modus wijst die af.
Base64 vs base64url: welk alphabet heb je nodig?
Standaard base64 gebruikt + en / plus =-padding, en dat is prima voor data:-URL's omdat die geen padsegmenten of querystrings bevatten. base64url (RFC 4648 §5) vervangt ze door - en _ en laat meestal padding weg, en het is verplicht zodra de output deel uitmaakt van een URL: JWT's gebruiken bijvoorbeeld base64url voor alle drie hun segmenten.
Wanneer base64url gebruiken
Gebruik base64url wanneer de gecodeerde string deel gaat uitmaken van een URL-pad of queryparameter, waar + en / escaping nodig hebben (en =-padding de querystring onoverzichtelijk maakt). Gebruik standaard base64 voor data-URI's, het insluiten van bytes in JSON-payloads, of alles wat geen URL is.
Hoe base64url produceren
Drie manieren, afhankelijk van de omgeving:
- Moderne browsers:
bytes.toBase64({ alphabet: 'base64url' }) - Node 16+:
Buffer.from(str).toString('base64url') - Post-processing: neem standaard output en vervang
+→-,/→_, en verwijder vervolgens de=
Een subtiel detail om te weten: RFC 4648 vereist dat encoders de non-data overflow-bits in het laatste chunk op nul zetten, en strikte decoders kunnen een fout gooien als dat niet gebeurd is. Het glossarium van MDN geeft het voorbeeld van 'VR==', wat dezelfde byte is als 'VQ==' maar met vuile padding-bits. Genereer base64 met een degelijke encoder en dit komt nooit voor; schrijf je er zelf één, dan kan het wel gebeuren.
Een bestand of afbeelding encoderen naar Base64 (data-URL's)
Voor bestanden is de browser-route FileReader met readAsDataURL(), wat een volledige data-URL produceert, waarna je het prefix verwijdert om de ruwe base64 te krijgen. Voor afbeeldingen die al in een canvas zijn gerenderd, doet toDataURL('image/png') dit in één aanroep.
FileReader.readAsDataURL() → data-URL → ruwe base64
function fileToDataURL(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.readAsDataURL(file);
});
}
const dataUrl = await fileToDataURL(fileInput.files[0]);
const rawBase64 = dataUrl.replace(/^data:\w+\/\w+;base64,/, '');
canvas.toDataURL() voor afbeeldingen
Als je een afbeelding op een canvas hebt getekend (resize, crop, wat dan ook), sla dan FileReader volledig over:
const dataUrl = canvas.toDataURL('image/png');
Dezelfde prefix-verwijdering geldt als je de kale base64-string nodig hebt.
Data-URL terug naar bytes
De omgekeerde richting is een fetch plus een array buffer. MDN's voorbeeld: een data-URL gebouwd uit new Uint8Array([0,1,2]) is 'data:application/octet-stream;base64,AAEC', en je herstelt de bytes zo:
const res = await fetch(dataUrl);
const bytes = new Uint8Array(await res.arrayBuffer());
// [0, 1, 2]
En de herinnering waarvoor dit hele artikel bestaat: roep btoa() niet aan op willekeurige tekst of HTML om data-URL's te bouwen. Het is een byte-naar-byte encoder, en input die niet Latin-1 is zal een fout gooien.
Node.js: Buffer.from(str, 'base64') — en het ene gevaarlijke verschil
Node heeft eigen idiomen die de TextEncoder-dans volledig overslaan: Buffer.from('Hello, world! 🌍').toString('base64') behandelt Unicode automatisch, omdat Buffer vanaf het begin op bytes werkt. Decodeer met Buffer.from(encoded, 'base64').toString('utf8').
Andere handige vormen: Buffer.from(str).toString('base64url') (Node 16+), en hele bestanden via fs.readFileSync('image.png').toString('base64').
const encoded = Buffer.from('Hello, world! 🌍').toString('base64');
const decoded = Buffer.from(encoded, 'base64').toString('utf8');
Node valideert je base64 niet (stille fouten)
Hier is het deel dat mensen verrast: de base64-decoder van Node valideert input niet. Buffer.from('!!!!', 'base64').toString() retourneert '' zonder fout, en Buffer.from('aGVsbG8!!!', 'base64') gooit de rommel er stilzwijgend uit en retourneert 'hello'. Vergelijk dat met atob('aGVsbG8!!!'), die een InvalidCharacterError gooit. Als de base64 van gebruikers, een API of een derde partij komt, valideer het dan voordat je decodeert in Node, of gebruik Uint8Array.fromBase64() met lastChunkHandling: 'strict' waar de runtimes dit ondersteunen.
Zijn btoa/atob verouderd? Browsers vs Node
Dit is echt tegenstrijdig, dus hier is de eerlijke oplossing. In browsers markeert MDN btoa() en atob() Baseline breed beschikbaar sinds juli 2015, niet verouderd. In Node markeren de type definitions ze als @deprecated met de opmerking dat ze "nooit gebruikt zouden moeten worden in nieuwe code", en bevelen Buffer.from(str, 'base64') aan.
Een goede regel: het is een legacy API, geen verwijderde. Prima in browsercode, prefereer Buffer in Node-specifieke code. Op basis van consensus in de community (geen specificatie-garantie) zijn btoa/atob ook de praktische keuze voor cross-environment als je één codepad nodig hebt dat ongewijzigd draait in browsers, Deno en Node zonder dependencies.
Wat je eigenlijk moet gebruiken
Als je één aanbeveling uit dit artikel meeneemt, zijn het de drie one-liners hieronder. Behoud btoa/atob voor gewone ASCII en legacy codebases, en gebruik de bytesToBase64 helper als je browser-fallback totdat toBase64() je hele publiek dekt.
// Modern browsers (2025+)
const enc = new TextEncoder().encode(str).toBase64();
const dec = new TextDecoder().decode(Uint8Array.fromBase64(b64));
// Browser fallback (MDN helper)
const encFallback = bytesToBase64(new TextEncoder().encode(str));
// Node (validate input first if it isn't yours)
const encNode = Buffer.from(str).toString('base64');
Voordat je uitrolt, verifieer je output met een snelle round trip in een browser-tool. Plak 'Hello, world!' in de gratis Base64 Encoder / Decoder van Tool Cloud, bevestig dat je 'SGVsbG8sIHdvcmxkIQ==' krijgt, decodeer het daarna terug en check of het identiek is. Er is ook een standalone Text to Base64 converter en Base64 to Text converter; bestanden worden automatisch verwijderd en er hoeft niets geïnstalleerd te worden.
Veelgestelde vragen
Waarom faalt btoa() op emoji maar werkt het wel op Engelse tekst?
btoa() behandelt elk teken als één byte en vereist dat elk codepunt onder de 256 ligt (betrouwbaar alleen tot 0x7F). Emoji, tekens met diakrieten en CJK-tekst hebben meerdere bytes nodig, waardoor ze een DOMException: InvalidCharacterError triggeren. Converteer eerst naar UTF-8-bytes met TextEncoder, encodeer daarna de bytes.
Is btoa() verouderd?
Niet in browsers — MDN markeert btoa() en atob() als breed beschikbaar sinds juli 2015. In Node markeren de type-definities ze echter als verouderd ten gunste van Buffer.from(str, 'base64') in Node-specifieke code. Het gaat om een legacy-API, geen verwijderde API.
Hoe encodeer ik Unicode-tekst (accenten, Chinees, emoji) naar Base64?
Gebruik new TextEncoder().encode(str) om UTF-8-bytes te krijgen, encodeer daarna de bytes (btoa() via een byte-string helper, of Uint8Array.toBase64() in moderne browsers). Om te decoderen gebruik je Uint8Array.fromBase64() of atob() om de bytes terug te krijgen, gevolgd door new TextDecoder().decode(bytes).
Wat is het verschil tussen base64 en base64url?
base64url (RFC 4648 §5) gebruikt '-' en '_' in plaats van '+' en '/', en laat vaak de '=' padding weg. Gebruik base64url wanneer de uitvoer in URLs of JWTs terechtkomt; standaard base64 is prima voor data: URLs, omdat deze geen padsegmenten of querystrings bevatten.
Waarom gooit Node.js geen fout bij ongeldige base64?
Node's Buffer.from(str, 'base64') valideert de invoer niet: het negeert ongeldige karakters stilzwijgend, dus Buffer.from('aGVsbG8!!!', 'base64').toString() retourneert 'hello' zonder foutmelding. In browsers gooit atob() een InvalidCharacterError bij dezelfde invoer. Valideer onbetrouwbare base64 voordat je decodeert in Node.