InvalidCharacterError: Failed to Execute 'atob' — Every Real Cause and Fix
The browser throws exactly one message for every broken call to atob(): InvalidCharacterError: Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded. It's unhelpful by design — the DOM spec doesn't say which character was wrong or where — which is why most fixes people reach for first are guesses. Some of those guesses (re-adding padding, stripping whitespace) don't actually do anything in a modern browser. This walkthrough separates the causes that really trigger the error from the ones that don't, verified live in this browser's own atob() rather than assumed from older answers, with a prefilled Base64 Suite example for each.
atob() implements the WHATWG "forgiving-base64" decode algorithm. It accepts only A–Z, a–z, 0–9, +, /, optional = padding, and ASCII whitespace (which it strips first). It throws if the string contains any other character, or if the decodable length — after stripping whitespace and padding — comes out to exactly 1 more than a multiple of 4, since that length can never represent a whole number of bytes.
Quick reference
Every result below was checked two ways in the same Chromium build: through this site's own Base64 Suite, and by calling window.atob() directly in the console — so "fails" and "works fine" both reflect the browser's actual behavior, not a guess from an old Stack Overflow thread.
| Symptom | Cause | Fix |
|---|---|---|
Fails, and re-adding = doesn't help | String was truncated mid-copy — decodable length is 1 more than a multiple of 4 | Get the untruncated string; no amount of padding recovers missing data |
| Fails only on tokens or IDs, never on your own test strings | Base64URL characters (-, _) passed to plain atob() | Convert first: str.replace(/-/g,'+').replace(/_/g,'/') |
| Fails immediately on anything pasted from a data URI | The data:image/png;base64, prefix was never stripped | Split on the comma: dataUri.split(',')[1] |
| Fails on one specific string, no obvious pattern | A stray non-Base64 character from a corrupted copy-paste or manual edit | Re-copy from source; don't hand-edit Base64 |
| You "fixed" it by re-adding padding, and it still fails | Padding was never the problem — see the myths section below | Check the four real causes above instead |
1. Truncated mid-copy — the string is missing data, not malformed
Base64 encodes 3 bytes as 4 characters. A correctly-encoded string's length, once whitespace and padding are stripped, is always a multiple of 4 — or 2 or 3 more than one (never exactly 1 more, since that would require a fractional byte). Cut a string short by copying from the wrong start point, an off-by-one substring, or a truncated network response, and there's a real chance you land on exactly that impossible length.
// A real 20-byte payload, correctly encoded, ends "...IQ==" (28 chars)
const full = "SGVsbG8sIGdldGRldnV0aWxzIQ==";
// Copied from the wrong offset — 3 characters short
const truncated = full.slice(0, 25); // "SGVsbG8sIGdldGRldnV0aWxzI"
atob(truncated);
// InvalidCharacterError: Failed to execute 'atob' on 'Window':
// The string to be decoded is not correctly encoded.
This is the one cause on this list where there's no client-side fix at all — the data itself is incomplete. Re-adding = characters won't help, because padding only ever accounts for the last group being 1 or 2 bytes short, not for entire groups missing from the middle or end. The real fix is upstream: find out why the string got cut (a substring bug, a truncated fetch, a copy that missed the tail) and get the complete value.
See it fail live
This is the truncated string above, decoded through Base64 Suite exactly as a browser's atob() would reject it:
2. Base64URL characters fed to plain atob()
Standard Base64 uses + and /. Base64URL — used by JWTs, and by anything meant to survive being embedded in a URL or filename — replaces them with - and _ and usually drops the = padding entirely. Neither - nor _ is in the alphabet atob() accepts, so a JWT segment, a URL-safe file ID, or any other Base64URL string throws the instant it hits - or _.
// A real Base64URL string (this exact bytes, URL-safe encoded)
const urlSafe = "BNridUsPzs1cs8leURtEuVg-lrU";
atob(urlSafe);
// InvalidCharacterError — the '-' isn't valid standard Base64
// Fix: convert the alphabet back before calling atob()
function base64UrlToBase64(s) {
let b64 = s.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '='; // atob tolerates missing padding, but this is cheap insurance
return b64;
}
atob(base64UrlToBase64(urlSafe)); // decodes correctly
This is the single most common way this error reaches production, because it's silent right up until it isn't: test data built from short ASCII strings often just happens not to contain a + or / in its encoded form, so the bug only shows up once a real token or ID does.
See it fail — then flip one switch and watch it pass
Same string, decoded twice. Toggle URL Safe in Base64 Suite and the identical input goes from throwing to decoding correctly — the fastest way to confirm this is the cause:
Fails: plain decodeWorks: with URL Safe enabled
3. The raw data: URI prefix, never stripped
A data URI — data:image/png;base64,iVBORw0KG... — is not itself valid Base64. Everything before the comma (data:, the MIME type, ;base64) is metadata that has to be split off first. Pass the whole string to atob() and it fails on the very first character, since : isn't in the Base64 alphabet either.
const dataUri = "data:text/plain;base64,SGVsbG8sIGdldGRldnV0aWxzIQ==";
atob(dataUri);
// InvalidCharacterError — fails on the "data:" prefix immediately
// Fix: split off the payload after the comma
const payload = dataUri.split(',')[1];
atob(payload); // decodes correctly
Tools that accept a pasted data URI directly do this split internally before ever calling atob() — which is exactly why pasting the full string into Base64 Suite below works fine, while calling atob() on it yourself in a fresh script won't, until you add the same split.
Try it live
Paste a full data URI straight into Base64 Suite — it strips the prefix and decodes the payload automatically, unlike a bare atob() call:
4. A stray character from a corrupted copy-paste
Less systematic than the first three, but common enough to check for: a single wrong character introduced by a bad copy (a smart-quote auto-substitution, a stray space turned into something else by an editor, a character lost to encoding mismatch when moving text between apps) is enough to make the whole string fail, even though everything around it is fine.
const corrupted = "SGVsbG8s!IGdldGRldnV0aWxzIQ=="; // one '!' where a letter should be
atob(corrupted);
// InvalidCharacterError — the '!' isn't in the Base64 alphabet
Unlike the truncation case, the data usually isn't gone — it's one character wrong. Re-copying from the original source (rather than trying to hand-edit the string back to something that looks right) is the reliable fix; guessing at the correct character is a coin flip you don't need to take.
See it fail live
Open this example in Base64 SuiteTwo popular "fixes" that don't actually do anything
Both of these show up constantly in older answers to this exact error. Neither one is what's actually wrong in a modern browser — verified directly against window.atob(), not assumed:
= padding back." Not anymore. atob() implements the WHATWG forgiving-base64 algorithm, which explicitly tolerates a string with its padding already removed, as long as the resulting length isn't 1 more than a multiple of 4. atob("SGVsbG8sIGdldGRldnV0aWxzIQ") — no padding at all — decodes correctly in every current browser. If padding were really the problem, restoring it wouldn't hurt, but it also isn't the fix; something else on this page is.
atob("SGVsbG8s\n IGdldGRl\tdnV0aWxzIQ==") decodes correctly, whitespace and all.
Both of these were true, or at least defensible, under older non-forgiving Base64 decoders. Cargo-culting them into a modern browser bug report just burns time that could go toward checking the four causes above instead.
Debugging checklist
- Log the string's length after stripping whitespace and
=. Iflength % 4 === 1, the string is missing data — stop here, there's no client-side fix, go find the truncation. - Check for
-or_anywhere in the string. If present, it's Base64URL — convert the alphabet (or just toggle URL Safe if you're using Base64 Suite) before decoding. - Check whether the string starts with
data:. If so, split on the first comma and decode only what comes after it. - If none of the above fit, diff the string against a known-good version of the same data — a single substituted or dropped character is the most likely remaining cause.
- Don't spend time re-adding padding or stripping whitespace as a first move — a modern browser's
atob()already tolerates both.
Related to this, but a different failure mode entirely: a JWT is Base64URL under the hood, so a malformed JWT error is often this exact same alphabet mistake, just surfaced by a JWT library's own parser instead of a raw atob() call.
📦 Paste your broken string and see exactly what's wrong
Base64 Suite encodes and decodes entirely in your browser — including automatic data: URI stripping and a one-click URL Safe toggle for Base64URL strings. Nothing you paste is ever uploaded.
Open Base64 Suite →