Base64 Image Not Showing: How to Find the Real Cause
2026-09-17 · 1081 words
A Base64 image that will not display is one of the least informative failures in web development. You get a broken-image icon, or an empty box, or nothing at all — and the browser console is silent. There is no exception to catch, because the browser did exactly what it was told: it tried to render a resource and gave up quietly.
The good news is that there are only six realistic causes, they are cheap to check in order, and each one produces a different symptom if you look closely.
Check 1: is the string actually Base64?
Before anything else, establish that the payload is in the right alphabet. Two count-based checks catch most of it:
const payload = s.includes(',') ? s.slice(s.indexOf(',') + 1) : s;
console.log('length % 4 =', payload.length % 4); // 1 means truncated
console.log('illegal =', (payload.match(/[^A-Za-z0-9+/=_-]/g) || []).join(''));
A remainder of 1 is fatal: no complete Base64 string can produce it, so characters were lost in transit. Everything else can usually be repaired. Illegal characters tell you what went wrong — a newline means MIME wrapping (see line breaks and padding), a %3D means the string went through a URL, and a - or _ means it is URL-safe Base64 (see URL-safe vs standard).
If the string is full of 0-9a-f and nothing else, it is hexadecimal. Decoding it as Base64 will produce bytes without error, and those bytes will not be an image.
Check 2: is the prefix complete?
The syntax is data:[<mediatype>][;base64],<data> and all three parts are load-bearing. Three prefix bugs produce three different symptoms:
- No
;base64flag.data:image/png,iVBORw0KGgo...makes the browser read the payload as literal text. You get an empty image box and no error, because you asked for a text file. - No comma.
data:image/png;base64followed by the payload is not a data URI at all. If your code strips "everything up to the first comma", the strip silently does nothing and the decoder chokes on the:in the header. - A
;base64typo such asbase64;or;base-64. Same as the first case: the payload is treated as text.
In a strict decoder these all fail loudly at a specific character position. In a browser they fail silently. That asymmetry is worth remembering when debugging: the browser is the least helpful place to ask "why".
Check 3: did it decode to any bytes at all?
A zero-byte result is a real outcome, and it has a specific cause: the payload contained nothing but padding characters, or the normalisation step removed everything it considered junk. Print the decoded length before you attempt to render:
const bytes = Uint8Array.from(atob(clean), (c) => c.charCodeAt(0));
console.log(bytes.length); // 0 → nothing to render, however good the string looks
If the length is in the hundreds of kilobytes, you have data. Whether it is an image is Check 4.
Check 4: are the bytes an image — and the right one?
This is where the declared media type gets tested against reality. Read the first bytes and compare:
89 50 PNG
FF D8 FF JPEG
47 49 46 38 GIF
52 49 46 46 WebP (with "WEBP" at offset 8)
1F 8B gzip — not an image
25 50 44 46 PDF
50 4B 03 04 ZIP
Two distinct failures live here. The first is that the bytes are not an image at all — gzipped JSON that was Base64-encoded to move it through a text-only channel is the classic case, and it is worth naming explicitly because "it decoded fine" makes people stop looking. The second is that the bytes are an image but a different one from what the header claims: data:image/jpeg;base64,iVBORw0KGgo... renders in most browsers, then fails in every tool that trusts the declared type. The bytes win in both cases, which is why our Base64 to Image tool prints the declared type and the signature side by side instead of picking one.
Check 5: is the image complete?
This is the cause people miss, and it produces the most confusing symptom: the image does display, then stops halfway. A grey band across the bottom of a photo, or a picture that renders correctly in a browser but is rejected by an image editor.
The reason is that JPEG has no total-length field in its header. A JPEG truncated at any point still looks like a perfectly valid JPEG as long as the markers parsed so far were legal, so decoders render what they have and stop. Truncation is common because Base64 travels through places that truncate: log lines with length caps, terminal selection that drops a wrapped line, database columns sized for the average case, chat messages with attachment limits.
The test is arithmetic, not visual:
- Decode the string and record the byte count.
- Compare it with the size the source reports for the original file.
- If the decoded count is lower, you have a truncated copy — go back to the source rather than trying to repair it.
For PNG the same check works, and there is an extra structural signal: a complete PNG ends with an IEND chunk. Reading the last twelve bytes and looking for 49 45 4E 44 (IEND) is a cheap completeness test:
const tail = bytes.subarray(-12);
console.log(new TextDecoder().decode(tail).includes('IEND'));
Check 6: is the browser refusing it for a reason unrelated to Base64?
The last two causes have nothing to do with the string, which is exactly why they are missed.
Content Security Policy. If your page sends img-src 'self' without data:, every data URI image is blocked — silently, and with no Base64 problem anywhere. The browser reports the violation to the console in most cases, but only if you know to look there, and tools that inject their own CSP by default (static hosts, reverse proxies) can add this without you noticing. Our own pages ship img-src 'self' blob: data: precisely for this reason.
Size. Data URIs are string data and live inside whatever document contains them. Very large ones — a few megabytes of Base64 in an inline <style> or a generated HTML file — hit parser and memory limits that vary by browser, and the failure looks like nothing at all happening. If a 40 KB icon works and a 4 MB photo does not, this is your answer, and the fix is a real URL instead.
The order matters
Run the checks in this order and stop at the first failure, because the later ones produce misleading results when the earlier ones are broken: a truncated string appears to "decode fine" and a wrong alphabet produces bytes that look like a corrupt image. Six checks, roughly a minute of work, and each one tells you a different thing to fix.
When you have the answer, the two repairs worth knowing are both structural rather than textual: pad the string if the remainder says it is short (safe, reversible), and go back to the source if the remainder is 1 or the decoded length does not match what the producer reported (not safe, not reversible, do not guess). Anything else — re-encoding, "trying" a different alphabet, appending bytes until it renders — is guessing at data you cannot see.