Double-encoded UTF-8: when ã� shows up instead of your text
There is a specific, recognizable flavor of mojibake that looks like a longer, stranger cousin of the familiar à pattern — strings such as ãÂ? or ’ where you expected an accented letter or a curly quote. This is double-encoded UTF-8: correct UTF-8 bytes that were misread once as a single-byte encoding and turned into characters, and then those wrong characters were encoded as UTF-8 again — producing valid UTF-8 that decodes perfectly, and is still wrong.
How the two-step corruption happens
A UTF-8 multi-byte character is a sequence of bytes, each of which is individually a valid byte value on its own. A single-byte encoding such as ISO-8859-1 or Windows-1252 assigns every one of those byte values to some character — it has no concept of a multi-byte sequence, so it happily decodes each byte of a UTF-8 character as its own separate accented Latin letter or symbol.
If a program reads UTF-8 bytes but is told, or defaults to assuming, a single-byte encoding, it produces exactly that: one UTF-8 character becomes two or three unrelated single-byte characters. That is ordinary mojibake, and it is the pattern covered separately in single-encode cases — the familiar ä, ö style corruption.
Double encoding is one further step: that already-wrong string of separate characters is then saved or transmitted through a step that encodes it as UTF-8 again. Because the wrong characters are themselves valid Unicode characters, this step succeeds without error and produces a longer, valid UTF-8 byte sequence — one that faithfully represents the wrong characters. Decode it correctly as UTF-8 and you get back exactly the wrong text, unchanged, because there is nothing invalid about it from that decoder's point of view.
// Single encoding (one wrong step): café → café // Double encoding (two wrong steps, both look "successful"): // 1. café (UTF-8 bytes: 63 61 66 C3 A9) // 2. decoded as Windows-1252 → "café" (5 characters, wrong but valid text) // 3. that string re-encoded as UTF-8 → new bytes for "café" // 4. decoded (correctly!) as UTF-8 → "café" — still wrong, and looks fine // to any tool that only checks "is this valid UTF-8?"
How to recognise it
The giveaway is that the corruption is longer and more layered than plain mojibake, often stacking two or three extra characters where one accented letter belongs — ’ in place of a single right single quotation mark is a common example, because that punctuation mark's UTF-8 byte sequence is three bytes long, and each of the three got its own single-byte misreading before the whole mess was re-encoded.
The other reliable signal is that the file passes UTF-8 validity checks with no errors at all — every byte sequence in a double-encoded file is legitimate UTF-8, just of the wrong characters. A validator that only confirms the bytes are well-formed UTF-8 will not flag this; you have to actually read the resulting text to notice it is wrong.
How to reverse it
The fix mirrors the corruption: undo the two steps in reverse. Take the double-encoded string, encode it back into bytes using UTF-8 (recovering the single-byte-decoded intermediate text as bytes), then decode those bytes using the single-byte encoding that caused the first misreading — commonly Windows-1252 or ISO-8859-1 — to recover the original correct text.
const doubled = "café"; const bytes = encodeWith(doubled, "UTF-8"); // bytes for the intermediate, wrong text const fixed = decodeWith(bytes, "Windows-1252"); // "café" — original recovered
Why it often happens twice
Double encoding tends to occur in pipelines with more than one boundary that each independently default to the wrong assumption — commonly a database column or API that defaults to a single-byte encoding, sitting between two systems that both correctly use UTF-8. The first boundary reads the UTF-8 input incorrectly and stores the resulting wrong text; the second boundary, seeing what it believes is ordinary text, correctly encodes that already-wrong text as UTF-8 for output. Neither step is malfunctioning on its own terms — each one does exactly what it was configured to do — which is exactly why this corruption survives multiple systems undetected until someone reads the actual text.
Stopping it from recurring
The lasting fix is the same principle as for single encoding: every boundary in the pipeline — database connection settings, API content-type declarations, file I/O calls — needs UTF-8 declared explicitly rather than left to a default, and those defaults are worth auditing specifically for the middle of a pipeline, not just its two visible ends, since that is exactly where a silent single-byte assumption tends to hide.