Why Your ñ Shows Up as ñ
Dev Tools

Why Your ñ Shows Up as ñ

The Symptom: A Name That Used to Say Something Else

A customer named "Muñoz" gets stored, exported, or emailed, and what comes back is "Muñoz." A degree symbol turns into "°." A curly apostrophe becomes a short run of boxes or question marks. None of this is a corrupted file in the sense of missing or scrambled data — every original byte is usually still sitting there, intact, on disk. What broke is the assumption about which alphabet those bytes belong to. This specific, recognisable garbling has a name, borrowed from Japanese: mojibake, literally "character transformation." It is not a catch-all term for "text looks wrong" — it describes one precise failure: bytes written under one encoding, then read back under a different one.

The reason mojibake is worth learning to read on sight, rather than just fixing by trial and error, is that the garbled output is not random. It is a deterministic function of the original bytes and the wrong encoding applied to them. Once you know the function, you can look at "ñ" and work backward to the original character with certainty, the same way you'd decode any other reversible transformation.

The Arithmetic Behind ñ

Start with the letter itself. The Spanish ñ carries a single fixed Unicode identifier, the code point U+00F1. That identifier is not a byte value in itself; it is simply the number the standard has settled on for this letter, and turning it into stored bytes is a separate job, done by an encoding.

Encode U+00F1 as UTF-8 and the rule is straightforward: any code point from U+0080 through U+07FF takes exactly two bytes, split as five bits in the first and six in the second. U+00F1 is 241 in decimal, which is 11110001 in 8-bit binary. Padded to the 11 bits the two-byte form uses, that string is 00011110001: the top 5 bits are 00011 and the bottom 6 bits are 110001. The first byte is the fixed prefix 110 plus those 5 bits: 11000011, which is 0xC3. The second byte is the fixed prefix 10 plus the remaining 6 bits: 10110001, which is 0xB1. So ñ, correctly encoded, comes out as the two bytes 0xC3 and then 0xB1 — a pair that, wherever it's stored, is already exactly right. Nothing has gone wrong yet.

The corruption happens one step later, when something reads those two bytes back and assumes they represent Latin-1 (ISO 8859-1) instead of UTF-8. Latin-1 is a single-byte encoding: every byte value maps to exactly one character, no multi-byte sequences at all. Look up byte 0xC3 in the Latin-1 table and you get à (capital A with a tilde), byte value 195. Look up 0xB1 and you get ± (the plus-minus sign), byte value 177. Two bytes that were meant to be read together, as one letter, get read apart, as two unrelated letters — and "ñ" is what lands on the screen. The bytes never changed. Only the rulebook used to interpret them did.

The Phantom  in Front of Things

A related pattern shows up constantly and looks, at first glance, like an unrelated bug: a stray  appears right before a degree sign, a non-breaking space, or a currency symbol, as in "25°C" instead of "25°C." It is the identical mechanism, just triggered by a different range of code points.

Every code point from U+0080 through U+00BF encodes in UTF-8 with the same leading byte: 0xC2. Take the degree sign, U+00B0 (176 decimal, 1011 0000). Padded to 11 bits that's 00010110000: top 5 bits 00010 give a first byte of 110 00010 = 0xC2, and bottom 6 bits 110000 give a second byte of 10 110000 = 0xB0. So ° in UTF-8 is 0xC2 0xB0. Read those two bytes as Latin-1: 0xC2 is Â, and 0xB0 happens to already be ° in the Latin-1 table too — so the visible result is "°," a phantom letter glued onto a symbol that otherwise looks correct. The same arithmetic explains a stray  before a non-breaking space (U+00A0, which encodes as 0xC2 0xA0): the space itself is invisible either way, so all you notice is the orphaned  sitting where nothing should be.

Four Places This Mismatch Actually Lives

Mojibake is a symptom, not a diagnosis. To fix it you have to find the one layer, out of several plausible ones, where a UTF-8 assumption and a Latin-1 assumption collided. In practice it is almost always one of four spots.

  • The database column and the connection charset. A table column can be declared with one character set (say, latin1) while the application sends bytes it believes are UTF-8 over a connection that hasn't been told otherwise. In MySQL, run SHOW FULL COLUMNS FROM your_table to see each column's declared charset, and SHOW VARIABLES LIKE 'character_set%' to see what the current connection is negotiating. A column stuck on latin1 receiving UTF-8 bytes will silently reinterpret them on the way in, on the way out, or both, depending on where the mismatch sits.
  • The HTTP Content-Type header. A response body encoded as UTF-8 but served with Content-Type: text/html; charset=ISO-8859-1 — or with no charset parameter at all, forcing the browser to guess — tells the receiving end to decode with the wrong table before a single byte is even inspected. Check it directly with curl -sI https://example.com/page and read the Content-Type line, or open your browser's network panel and inspect the response headers on the actual request.
  • A file read without an explicit encoding. Plenty of language runtimes fall back to a platform default when no encoding is named — Python's open() uses the locale's preferred encoding unless you pass encoding="utf-8"; Java's charset-less FileReader constructors read with Charset.defaultCharset(), which followed the platform locale until JEP 400 made UTF-8 the JVM-wide default in JDK 18 (March 2022) — so on a current JDK this one no longer bites by itself, but it still does on an older runtime, or on any JDK 18 or later started with -Dfile.encoding=COMPAT to restore the old locale-derived behaviour. A UTF-8 file read under a different silent default gets misinterpreted the moment it's loaded, before your code has done anything else to it.
  • A CSV opened straight in a spreadsheet. Older versions of Excel, and even current ones on a plain double-click, guess a file's encoding rather than asking, and that guess is frequently the system's regional codepage rather than UTF-8. The fix is to import deliberately — Data → From Text/CSV in Excel — and pick UTF-8 explicitly in the wizard, or save the export with a UTF-8 byte-order mark (the three bytes 0xEF 0xBB 0xBF at the very start of the file), which most spreadsheet software will detect and honor automatically.
Why Your ñ Shows Up as ñ

Confirming the Diagnosis by Looking at the Bytes

Guessing from the rendered garble alone invites mistakes, because more than one wrong-encoding pairing can produce similar-looking noise. The reliable way to confirm what happened is to stop looking at the rendered text and look at the raw bytes instead. On the command line, xxd suspect_file.txt | head or od -An -tx1 suspect_file.txt dumps the file's actual byte values in hex, independent of how any particular editor or terminal chooses to display them.

With the raw bytes in front of you, the check is arithmetic, not guesswork: if you expected an ñ and you see the byte pair c3 b1, the file is correctly UTF-8 encoded and the corruption is happening downstream, at render or display time. If instead you see a single byte like f1, the file itself was saved as Latin-1 (or Windows-1252) and never became UTF-8 in the first place — a genuinely different problem with a different fix. Pasting the suspect text into a tool that shows the UTF-8 byte sequence for each character — our own ASCII & Unicode converter lists it per character alongside the code point — gets you the same answer without a terminal, and lets you compare what you typed against what a database or API actually returned.

Why Double-Encoding Doesn't Always Undo Cleanly

Sometimes the two-byte sequence 0xC3 0xB1 doesn't just get displayed wrong — it gets saved wrong, permanently, by a second pass through the same mistake. If a system reads UTF-8 bytes as Latin-1, producing the two characters à and ±, and then that two-character string is itself encoded as UTF-8 and written back to storage, you now have four bytes on disk where two used to be: à (U+00C3) becomes 0xC3 0x83, and ± (U+00B1) becomes 0xC2 0xB1, giving the stored sequence 0xC3 0x83 0xC2 0xB1. This is "double-encoded" UTF-8, and it can, in principle, be walked back: decode the four bytes as UTF-8 to recover the two characters à and ±, then take their code points — 0xC3 and 0xB1 — treat those numbers as raw byte values again, and decode that pair as UTF-8 to land back on ñ.

That reversal only works, though, when every byte along the way maps to a character that Latin-1 (or its close cousin Windows-1252, which many "Latin-1" decoders actually use) actually defines. Windows-1252 has five byte values — 0x81, 0x8D, 0x8F, 0x90, and 0x9D — that correspond to no character at all in its table. A UTF-8 byte that happens to land on one of those undefined slots during a mis-decode typically gets swapped out for the Unicode replacement character, U+FFFD, rather than passed through. That substitution is not reversible, because U+FFFD is used to stand in for many different unrepresentable bytes — once several different original values have all been collapsed onto the same placeholder character, there is no way to tell, from the placeholder alone, which one you started with. The information is genuinely gone, not just mis-rendered, which is the real distinction between a display bug you can fix after the fact and a data-loss bug you cannot.

A Practical Checklist for the Next One

  • Recognise the shape first: a repeated à or  followed by an unrelated symbol is close to always UTF-8 bytes read as Latin-1 or Windows-1252 — not a font problem, not a "special characters" problem in the vague sense.
  • Confirm with a hex dump or a byte-level tool before changing anything. If the stored bytes are already wrong (a single byte where a two-byte sequence belongs), the fix is at the point of writing, not at the point of display.
  • Check the database column charset and the live connection charset separately — they can disagree even when both look reasonable in isolation.
  • Check the actual HTTP response header, not just what your templating engine claims to output, since a missing or wrong charset parameter overrides everything downstream.
  • Never open an exported CSV with a plain double-click when it needs to survive a round trip through a spreadsheet; use the encoding-aware import path instead.
  • If the text has already been through a lossy re-encoding and shows the Unicode replacement character anywhere, treat that portion as unrecoverable rather than spending time trying to reverse it.

For the deeper reasoning behind code points, UTF-8, and why this class of bug exists at all, see ASCII, Unicode and UTF-8: planes, normalization and sort order. For the invisible-character cousin of this problem — bytes that don't render as the wrong letter but instead behave like the wrong instruction — see our companion piece on LF, CR, and NUL, and what each one does to a file.

← Back to Blog