InvalidCharacterError: Failed to execute 'atob' — The string to be decoded is not correctly encoded

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

CauseSymptomFix
Data URLString starts with data:Decode only the part after the comma
base64url (JWT)Contains - or _Convert to standard base64 first
WhitespaceCopied from a PEM block or email headerStrip with replace(/\s/g, '')
UnicodeOutput is mojibake after decodingRe-decode the bytes with TextDecoder
Not base64 at allPlain text or a URL passed inConfirm 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

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:

Environmentatob / btoa
Chrome / Edge✅ Supported
Firefox✅ Supported
Safari✅ Supported
Node.js 16+✅ Supported (global); Buffer is idiomatic
Deno / Bun✅ Supported

References

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.

Base64 Encoder / Decoder JWT Decoder All Error References
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.