Quick answer
atob() only accepts standard base64 (A–Z a–z 0–9 + / with = padding). It throws when the input contains anything else — a data: URI prefix, base64url characters (- _, as in JWTs), or whitespace. In practice the URL-safe characters are the usual trigger. Clean the string to valid base64 first.
The exact error string
atob('data:image/png;base64,iVBORw0KGgo=');
// Uncaught InvalidCharacterError: Failed to execute 'atob' on 'Window':
// The string to be decoded is not correctly encoded.
// A JWT signature segment is base64url and usually contains '-' or '_':
atob('SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c');
// Same error — '_' is not part of the standard base64 alphabet
The message is generic, but the cause is always the same: atob met a character that is not part of the standard base64 alphabet. The fix is to normalise the input before decoding.
Why atob is strict about its input
atob ("ASCII to binary") decodes standard base64 as defined by RFC 4648: the 64 symbols A–Z, a–z, 0–9, +, /, plus = for padding. It does not trim, does not accept the URL-safe alphabet, and does not understand any wrapper. Anything outside that set is a decode error, not a warning.
At a glance: cause, symptom, fix
| Cause | Symptom | Fix |
|---|---|---|
| Data URL | String starts with data: | Decode only the part after the comma |
| base64url (JWT) | Contains - or _ | Convert to standard base64 first |
| Whitespace | Copied from a PEM block or email header | Strip with replace(/\s/g, '') |
| Unicode | Output is mojibake after decoding | Re-decode the bytes with TextDecoder |
| Not base64 at all | Plain text or a URL passed in | Confirm the value is actually base64 |
Cause 1: a data URL prefix
The most common mistake is feeding the whole data URL to atob. The data:...;base64, part is metadata, not base64 — keep only what follows the comma:
const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg==';
atob(dataUrl); // ❌ throws — the prefix isn't base64
const b64 = dataUrl.split(',')[1]; // 'iVBORw0KGgoAAAANSUhEUg=='
atob(b64); // ✅ decodes the image bytes
Cause 2: base64url (JWTs and tokens)
JWTs, URL parameters, and many APIs use base64url, which swaps +→- and /→_ and usually drops the = padding. The reason for the variant: in standard base64, +, /, and = are awkward inside URLs and HTTP headers (+ means a space in a query string, / is a path separator, = is reserved) — base64url replaces them with URL-safe characters. Convert it back to standard base64 before atob:
function fromBase64Url(s) {
s = s.replace(/-/g, '+').replace(/_/g, '/'); // url-safe -> standard
while (s.length % 4) s += '='; // restore padding
return atob(s);
}
const sig = 'SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; // JWT signature
atob(sig); // ❌ throws — '_' is not standard base64
fromBase64Url(sig); // ✅ decodes (returns the raw signature bytes)
If you are decoding a JWT to read its header or payload, you do not have to hand-roll this — paste the token into the JWT Decoder, which handles base64url and shows each segment. (And never trust a decoded JWT for auth — see invalid signature & jwt malformed.)
Cause 3: whitespace and newlines
Base64 pulled from a PEM file, an email header, or a pretty-printed blob often carries line breaks. Strip all whitespace first:
const pem = `MIIBIjANBgkqhkiG9w0BAQ
EFAAOCAQ8AMIIBCgKCAQEA`; // wrapped at 64 cols
atob(pem); // ❌ newlines are not base64
atob(pem.replace(/\s/g, '')); // ✅ whitespace removed
Cause 4: Unicode and emoji
Here is the part that catches everyone: atob does not return UTF-8 text. It returns the raw decoded bytes, packed one-per-character into an ordinary JavaScript string (each character a code point 0–255). For ASCII that happens to look right, but multibyte UTF-8 comes out as mojibake unless you re-interpret those bytes as UTF-8 yourself:
const b64 = 'w6nDqQ=='; // "éé" encoded as UTF-8
atob(b64); // 'éé' — wrong (raw bytes as Latin-1)
// Decode the bytes as UTF-8:
const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
new TextDecoder().decode(bytes); // ✅ 'éé'
// Modern runtimes: Uint8Array.fromBase64(b64, { alphabet: 'base64url' })
Node.js: prefer Buffer
In Node, atob exists but Buffer is idiomatic and handles UTF-8 directly — and accepts base64url as an encoding name:
Buffer.from(b64, 'base64').toString('utf8'); // standard base64 -> text
Buffer.from(token, 'base64url').toString('utf8'); // url-safe, no manual swap
Quick check: is the string valid base64?
Two one-liners catch the common cases before you ever call atob:
// Valid STANDARD base64? (what atob accepts)
/^[A-Za-z0-9+/]*={0,2}$/.test(input);
// Valid base64URL instead? (allows - and _ ; this is a JWT segment)
/^[A-Za-z0-9_-]*={0,2}$/.test(input);
input.length % 4; // 0 for well-padded standard base64
Debugging checklist
- ✓ Does the string start with
data:? Decode only the part after the comma - ✓ Does it contain
-or_? It's base64url — swap to+//and re-pad - ✓ Any spaces or newlines? Strip them with
replace(/\s/g, '') - ✓ Length not a multiple of 4? Re-pad with
=(defensive — modern browsers tolerate missing padding, so the usual culprit is-/_, not the absent=) - ✓ Non-ASCII output looks wrong? Decode the bytes with
TextDecoder - ✓ In Node? Use
Buffer.from(s, 'base64'|'base64url')
Frequently Asked Questions
What does 'Failed to execute atob: the string to be decoded is not correctly encoded' mean?
It means you passed atob() a string that is not valid standard base64. atob only accepts the characters A-Z, a-z, 0-9, + and /, with optional = padding. Any other character — a space, newline, the URL-safe - or _, or a data: URI prefix — makes it throw InvalidCharacterError.
Why does atob fail on a JWT or URL-safe base64?
JWTs and many tokens use base64url, a variant that replaces + with - and / with _ and usually drops the = padding. atob only understands standard base64, so it rejects - and _. Convert base64url to standard base64 first: replace - with +, _ with /, and re-add padding to a multiple of 4.
How do I decode a data URL with atob?
A data URL like data:image/png;base64,iVBOR... contains a prefix that is not base64. Strip everything up to and including the comma first: const b64 = dataUrl.split(',')[1]; then call atob(b64). Passing the whole data URL throws because of the data:...;base64, prefix.
Why does atob break on Unicode or emoji?
atob returns a binary string where each character is one byte (0–255). For Unicode you must decode the bytes as UTF-8 afterwards: take the atob output, build a Uint8Array from its char codes, then new TextDecoder().decode(bytes). Skipping that step gives mojibake, and encoding multibyte text with btoa directly throws a different InvalidCharacterError.
Does whitespace cause atob to fail?
It can. Base64 copied from a PEM file or wrapped email header often contains newlines or spaces. Strict atob implementations reject them. Remove all whitespace before decoding: atob(input.replace(/\s/g, '')).
Is atob deprecated, and what should I use in Node.js?
atob and btoa exist in modern browsers and Node, but in Node the idiomatic tool is Buffer: Buffer.from(b64, 'base64').toString('utf8'). New runtimes also offer Uint8Array.fromBase64() with a base64url option, which handles the URL-safe variant without manual conversion.
Browser and runtime support
atob and btoa are available everywhere you are likely to run them — the error is about the input, not missing support:
| Environment | atob / btoa |
|---|---|
| Chrome / Edge | ✅ Supported |
| Firefox | ✅ Supported |
| Safari | ✅ Supported |
| Node.js 16+ | ✅ Supported (global); Buffer is idiomatic |
| Deno / Bun | ✅ Supported |
References
- RFC 4648 — The Base16, Base32, and Base64 Data Encodings (defines the standard and URL-safe alphabets)
- WHATWG HTML Standard — the
atob()/btoa()specification
Decode or encode base64 in your browser
Paste a base64 (or base64url) string into the Base64 tool to decode it instantly — nothing is uploaded to a server.