How to Decode Base64 Strings in JavaScript
JavaScript has built-in Base64 support in both browsers and Node.js, but the APIs differ between environments and each has edge cases that trip up developers. The browser's atob() and btoa() functions are simple but silently fail on non-Latin characters. Node.js Buffer handles both text and binary cleanly. TextDecoder solves the Unicode problem. This guide covers every Base64 decode scenario you will encounter in JavaScript: plain text, Unicode strings, binary files, data URIs, and Base64url — with working code examples for each.
Decoding Base64 in the Browser with atob()
The browser provides two global functions for Base64: btoa() (binary to ASCII — encodes) and atob() (ASCII to binary — decodes). These have been available in all major browsers since Internet Explorer 10. Basic usage: atob('SGVsbG8gV29ybGQ=') returns the string 'Hello World'. For simple ASCII or Latin-1 text, this works perfectly. The critical limitation of atob() is that it treats the decoded output as a binary string — a JavaScript string where each character represents one byte, with character codes ranging from 0 to 255. This works fine for ASCII text and binary data, but it breaks for text that contains characters outside the Latin-1 range (characters with code points above 255), because those characters require multiple bytes in UTF-8 encoding. If you Base64-encode a Japanese or emoji string, then try to decode it with atob(), you will get a garbled binary string rather than the original text. The fix is to use atob() to get the binary string, then use TextDecoder to interpret those bytes as UTF-8. The pattern looks like this: decode the Base64 with atob(), convert the binary string to a Uint8Array by mapping each character to its char code, then pass that array to new TextDecoder('utf-8').decode(). This two-step process reliably decodes any Base64-encoded UTF-8 text. For the reverse — encoding Unicode text to Base64 — you need to UTF-8 encode the string to bytes first (using TextEncoder), then convert those bytes to a binary string, then call btoa().
Decoding Base64 in Node.js with Buffer
Node.js does not have atob() and btoa() in older versions (they were added as globals in Node 16 but exist mainly for browser compatibility). The idiomatic Node.js approach uses the Buffer class, which handles binary data cleanly and correctly. To decode a Base64 string to UTF-8 text: Buffer.from(base64String, 'base64').toString('utf8'). This correctly handles multi-byte UTF-8 characters, including emoji and international scripts, without any extra steps. To decode a Base64 string to a binary file and save it: Buffer.from(base64String, 'base64') gives you a Buffer (Node's version of a binary array). Pass it to fs.writeFileSync('output.pdf', buffer) to save it as a file. The Buffer class correctly handles all byte values from 0 to 255. To encode text or binary to Base64: Buffer.from('Hello World', 'utf8').toString('base64'). For a file: Buffer.from(fs.readFileSync('image.jpg')).toString('base64'). Node.js Buffer also supports Base64url encoding directly: Buffer.from(data).toString('base64url') produces a URL-safe Base64 string without + or / characters and without padding. This is the correct encoding to use when generating JWTs, PKCE code verifiers, or other URL-safe tokens without reaching for an external library.
Handling Data URIs and Binary Files
When working with images, PDFs, or other binary files in the browser, Base64 typically appears as a data URI. The FileReader API returns data URIs from readAsDataURL(). Canvas.toDataURL() returns them too. A data URI looks like: data:image/png;base64,iVBORw0KGgo... To extract the raw Base64 string from a data URI, split or substring past the comma: const base64 = dataUri.split(',')[1]. The part before the comma is the MIME type and encoding declaration — you need only what follows. To go the other way — convert a Base64 string back to a binary Blob in the browser for download or display: decode the Base64 to binary bytes using atob(), create a Uint8Array from those byte values, and then wrap it in new Blob([uint8Array], {type: 'image/png'}). Passing that Blob to URL.createObjectURL() gives you a URL you can assign to an image src or an anchor href for download. For the fetch API, there is a common shortcut: if you already have a data URI, you can fetch it directly — fetch(dataUri).then(r => r.blob()) — and the browser will decode the Base64 and return the binary Blob for you. This is cleaner than manual byte manipulation. In Node.js, converting Base64 to a Buffer and writing it to disk is the most common pattern: require('fs').writeFileSync('output.png', Buffer.from(base64String, 'base64')). No data URI handling is needed in Node.js because the FileReader API is browser-only.
Working with Base64url and Common Pitfalls
Base64url is used in JWTs, OAuth PKCE, and URL-safe tokens. It replaces + with - and / with _ and omits trailing = padding. Standard atob() and Buffer.from(str, 'base64') handle Base64url inconsistently across environments — you need to normalize the string first. To decode Base64url reliably in any JavaScript environment: replace all - with + and all _ with /, then add the correct number of = padding characters (padding length is (4 - (str.length % 4)) % 4), then decode with your preferred method. Alternatively, in Node.js 16+, Buffer.from(str, 'base64url') handles this automatically. Common pitfalls in JavaScript Base64 work: Whitespace in the input: if your Base64 string has line breaks or spaces (common in MIME email or pretty-printed config files), atob() will throw. Strip all whitespace with str.replace(/\s/g, '') before decoding. Using btoa() on non-Latin-1 characters: btoa() throws InvalidCharacterError for any character with a code point above 255. Always UTF-8 encode first using TextEncoder before calling btoa(). Not checking padding: if you receive a Base64 string from an API that stripped trailing padding, add the = characters back. A string of length 4n+1 is never valid Base64 — that should be 4n+2, 4n+3, or 4n characters. For a quick online way to test encode and decode operations without writing code, the WikiPlus Base64 tool handles text and files entirely in the browser with correct UTF-8 support.
Frequently Asked Questions
- Why does atob() give wrong results for text with emoji or Japanese characters?
- atob() treats the decoded output as a Latin-1 binary string, not UTF-8 text. Emoji and non-Latin characters require multiple bytes in UTF-8, so atob() returns the raw bytes as individual characters rather than the intended text. The fix is to use atob() to get the binary bytes, convert them to a Uint8Array, and then decode with new TextDecoder('utf-8').decode(). In Node.js, use Buffer.from(str, 'base64').toString('utf8') which handles UTF-8 correctly by default.
- Is there a difference between atob() in the browser and Buffer in Node.js?
- Yes, meaningfully so. atob() is synchronous and returns a binary string (each character is one byte). Buffer.from(str, 'base64') also decodes synchronously but returns a true binary buffer that correctly handles all byte values and integrates cleanly with file system writes and streams. In browser environments, atob() is the right primitive; pair it with TextDecoder for UTF-8. In Node.js, always prefer Buffer. Both browser globals atob/btoa were added to Node 16 mainly for compatibility, but they are not idiomatic Node.js.
- How do I encode an image file to Base64 in JavaScript for an API call?
- In the browser, use FileReader.readAsDataURL(file) in a Promise wrapper. When it resolves, split the result at the comma to get the raw Base64 string. In Node.js, read the file with fs.readFileSync() and call .toString('base64') on the Buffer. In both cases, check whether the API expects the full data URI format with MIME type prefix or just the raw Base64 string — most REST APIs want the raw string only.