Why does atob fail with "The string to be decoded is not correctly encoded"?

Dark navy cover card with the title 'Why does atob fail with "The string to be decoded is not correctly encoded"?' in white and a green accent bar

atob throws "The string to be decoded is not correctly encoded" when its input is not plain, standard Base64. The usual causes are Base64URL characters (- and _), a data: URI prefix, URL-encoded %3D padding, too many = signs, or a truncated string. Clean the input first, and the atob not correctly encoded error goes away.

What the atob "not correctly encoded" error means

The atob error means the browser's Base64 decoder returned "failure", and the browser turned that into an InvalidCharacterError. The wording depends on the browser, but the check behind it is the same everywhere.

The HTML Standard defines atob() in two lines: run the forgiving-base64 decoder, and "If decodedData is failure, then throw an "InvalidCharacterError" DOMException." MDN's atob page is shorter: the error is "Thrown if encodedData is not valid base64."

Chrome and Edge show this as:

Uncaught DOMException: Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.

Firefox throws the same exception with a different message, "String contains an invalid character". A Mozilla bug report points out that this message can be misleading, because Firefox shows it for atob("+"), where every character is valid.

The four rules atob checks

atob runs the WHATWG "forgiving-base64 decode" algorithm, and the input fails if it breaks any of four rules. The Infra Standard lists the steps:

  1. Whitespace is removed first. Spaces, tabs and newlines never cause the error.
  2. Padding is stripped only when the length is a multiple of 4. In that case, one or two trailing = are removed.
  3. A length that leaves a remainder of 1 when divided by 4 fails. No valid Base64 string has that length.
  4. Anything other than +, /, letters and digits fails. That includes -, _, %, :, , and any = left over from step 2.

Rule 3 explains the Firefox bug above. "+" has a length of 1, so it fails on length, even though + is a legal character.

Rule 2 is why missing padding passes and extra padding fails.

Which inputs fail, tested

These are real results from running atob() on each input. Node.js uses the same algorithm as browsers, so the outcome matches Chrome and Firefox.

Input Result Why
SGVsbG8= Hello Valid Base64
SGVsbG8 Hello Missing padding is allowed
SGV sbG8= Hello Whitespace is ignored
SGVsbG Hell Shorter but still a valid length
SGVsbG8== Throws Length is not a multiple of 4, so the = stay and fail rule 4
SGVsbG8_Pg Throws Base64URL uses _ (standard is SGVsbG8/Pg==)
data:text/plain;base64,SGVsbG8= Throws :, ; and , from the data URI prefix
SGVsbG8%3D Throws %3D is a URL-encoded =
+ Throws Length remainder of 1

Common causes and how to fix each

Most cases of this error come from passing atob something that is close to Base64 but not quite.

Base64URL from a JWT or URL

Base64URL swaps + and / for - and _, as RFC 4648 defines. JWT segments, OAuth values and many URL tokens use it, and atob rejects both characters.

A JWT header such as eyJhbGciOiJIUzI1NiJ9 may decode fine by luck, because it happens to contain no - or _. The next token you try will. Swap the characters back before decoding. Our post on Base64 vs Base64URL covers the difference, and decoding a JWT without the secret shows the full token case.

A data URI prefix

A data URI such as data:image/png;base64,iVBOR... is not Base64 on its own. Only the part after the comma is. Passing the whole string to atob fails on the colon.

Split on the first comma and decode the second half.

URL-encoded padding

When Base64 travels in a query string, = often arrives as %3D and + as %2B. Run decodeURIComponent() on the value before atob.

Watch for the reverse problem too. A form or query parser may turn + into a space. atob then ignores the space, drops a character and either fails on length or decodes the wrong bytes.

Extra or cut-off characters

Copying from a log, a terminal or a chat window often adds a stray = or cuts the string short. If the length leaves a remainder of 1 when divided by 4, part of the value is missing, and no cleanup will recover it. Go back to the source.

Unicode text

atob itself does not fail on Unicode output, but it returns garbage. MDN describes the result as "A binary string containing raw bytes", one character per byte. The Base64 Y2Fmw6k= holds the UTF-8 bytes of café, and atob returns café.

The encoding side does fail. The HTML Standard says btoa() throws InvalidCharacterError for any character above U+00FF, so btoa('café ✓') throws before you ever get a string to decode.

A decode function that handles all of them

This function cleans the common problems and then decodes the bytes as UTF-8. I tested it on every input in the table above: it decodes the valid ones and the Base64URL, data URI, %3D and extra-padding cases, and rejects +.

function decodeBase64(input) {
  let s = String(input).trim();
  if (s.startsWith('data:')) s = s.slice(s.indexOf(',') + 1); // drop "data:...;base64,"
  if (/%[0-9a-f]{2}/i.test(s)) s = decodeURIComponent(s);     // undo URL encoding
  s = s.replace(/\s+/g, '')                                   // newlines from PEM or email
       .replace(/-/g, '+').replace(/_/g, '/')                 // Base64URL to Base64
       .replace(/=+$/, '');                                   // let atob handle padding
  if (s.length % 4 === 1) throw new Error('Base64 input is truncated');
  const bytes = Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);                     // UTF-8 text
}

The limitation: this function is forgiving on purpose, so it will also accept input that a strict server would reject. If you are validating data rather than reading it, don't clean it first. Reject it and log the reason.

Use Uint8Array.fromBase64 when you can

Uint8Array.fromBase64() is the newer replacement for atob, and it handles Base64URL directly. MDN's fromBase64 page says it ignores ASCII white space and accepts an alphabet: "base64url" option for - and _.

It returns bytes instead of a byte string, and it throws a SyntaxError with a clearer cause. Its lastChunkHandling option lets you choose "loose" (the default), "strict" or "stop-before-partial" for the padding rules.

const bytes = Uint8Array.fromBase64('SGVsbG8_Pg', { alphabet: 'base64url' });
new TextDecoder().decode(bytes); // "Hello?>"

My take: write new browser code with fromBase64 and leave atob to old code paths. The catch is support. Older browsers and older Node versions don't have it, so check your targets or keep the function above as a fallback.

If you only need to see what a string contains, the OnlineViewer Base64 decoder is UTF-8 safe and reads Base64URL, data URIs and PEM blocks in the browser, without sending your input anywhere. For a terminal route, see how to decode Base64 on the command line. And if the string turns out to hold a password, read is Base64 encryption before you paste it anywhere.

FAQ

Does atob need padding?

No. atob accepts Base64 with or without the = padding. It only strips padding when the length is a multiple of four, so too many = characters make it fail, while missing ones are fine.

Why does atob work in Chrome but the message is different in Firefox?

Both browsers run the same WHATWG algorithm and throw an InvalidCharacterError DOMException. Chrome words it as 'The string to be decoded is not correctly encoded', and Firefox says 'String contains an invalid character'.

Can atob decode Base64URL?

No. atob only accepts +, / and letters and digits. Replace - with + and _ with / first, or use Uint8Array.fromBase64 with the base64url alphabet option.

Why does atob return strange characters instead of throwing?

atob returns one character per byte, so UTF-8 text with accents or emoji comes back garbled, such as café for café. Turn the result into bytes and decode it with TextDecoder.

Sources