InvalidCharacterError: Failed to Execute 'atob' — Every Real Cause and Fix

Published 10 Sep 2026 · 7 min read

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.

Quick answer: 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.

SymptomCauseFix
Fails, and re-adding = doesn't helpString was truncated mid-copy — decodable length is 1 more than a multiple of 4Get the untruncated string; no amount of padding recovers missing data
Fails only on tokens or IDs, never on your own test stringsBase64URL characters (-, _) passed to plain atob()Convert first: str.replace(/-/g,'+').replace(/_/g,'/')
Fails immediately on anything pasted from a data URIThe data:image/png;base64, prefix was never strippedSplit on the comma: dataUri.split(',')[1]
Fails on one specific string, no obvious patternA stray non-Base64 character from a corrupted copy-paste or manual editRe-copy from source; don't hand-edit Base64
You "fixed" it by re-adding padding, and it still failsPadding was never the problem — see the myths section belowCheck 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:

Open this example in Base64 Suite

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 decode
Works: 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:

Open this example in Base64 Suite

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.

Two 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:

Myth 1 — "You need to add the missing = 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.
Myth 2 — "Strip whitespace and line breaks before decoding." Also unnecessary. The same forgiving-base64 algorithm strips ASCII whitespace (spaces, tabs, newlines, carriage returns, form feeds) from anywhere in the string before decoding — including the line breaks in a MIME-style, 76-characters-per-line Base64 block copied straight out of an email attachment or a PEM file. 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

  1. Log the string's length after stripping whitespace and =. If length % 4 === 1, the string is missing data — stop here, there's no client-side fix, go find the truncation.
  2. 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.
  3. Check whether the string starts with data:. If so, split on the first comma and decode only what comes after it.
  4. 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.
  5. 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 →

← Back to Blog · See also: JWT Malformed →