Primary Error All articles
War Stories

Mojibake: The Encoding Bug That Silently Trashes Your Data Before You Even Know It's Gone

Primary Error
Mojibake: The Encoding Bug That Silently Trashes Your Data Before You Even Know It's Gone

Photo: Mike.lifeguard, CC BY-SA 4.0, via Wikimedia Commons

There's a special category of bug that doesn't blow up your stack trace or set off your monitoring alerts. It doesn't page you at 3 AM. It just sits there, patient and invisible, turning your customer's name into élise or your product description into a wall of question marks and garbage characters. By the time someone notices, you've got corrupted database rows, broken API contracts, and a support queue full of confused users who can't figure out why their account name looks like it was typed by a cat.

Welcome to the world of character encoding bugs. Population: every developer who ever assumed UTF-8 "just works."

What Even Is Mojibake?

Mojibake — a Japanese word that roughly translates to "character transformation" — is what happens when text gets encoded with one character set and decoded with another. The bytes are technically all there. Nothing is technically lost. But the interpretation is completely wrong, and the result is unreadable nonsense.

The most common version of this story goes like this: your app sends a string over the network encoded as UTF-8. The receiving system — maybe a legacy API, maybe a MySQL table that was created before someone remembered to set the charset, maybe a third-party vendor running software from 2009 — interprets those bytes as Latin-1 (ISO-8859-1). Now every non-ASCII character is mangled. An é becomes é. A ü becomes ü. An emoji? Don't even ask.

The worst part is that neither system throws an error. Both sides believe they did their job correctly. The bytes moved. The transaction completed. Everything looks fine on the surface.

The Assumption That Breaks Everything

Here's the primary error: developers assume encoding is a solved problem. UTF-8 is the dominant standard, browsers default to it, most modern frameworks handle it automatically, so why would you even think about it?

Because not everything is modern. Because you're talking to that one payment processor that hasn't updated their integration docs since the Obama administration. Because your company acquired a startup six months ago and their database collation is set to latin1_swedish_ci — the MySQL default that's caused more silent data corruption than probably any other single configuration choice in software history. Because the CSV export your client sends every Monday morning was generated by a Windows machine running Excel, which loves to use Windows-1252 encoding and doesn't feel the need to mention that anywhere in the file.

The moment you cross a system boundary — any boundary — you have an encoding contract. Most of the time nobody wrote that contract down. Everybody just assumed.

A Real-World Cascade

Let's walk through how this actually plays out in production, because the textbook version doesn't capture how disorienting it is in real life.

Your team builds an e-commerce integration that pulls product data from a supplier's REST API. The API documentation says it returns JSON. JSON is supposed to be UTF-8. You test with a handful of products, everything looks great, you ship it.

Three weeks later, your product catalog has a few hundred items with garbled titles. Some have broken descriptions. A few have invisible characters embedded in their SKUs that cause downstream matching logic to fail silently. Your search index is partially corrupted because the indexer choked on malformed strings and just... skipped them, logging a warning that nobody reads.

You spend two days assuming it's a data quality issue on the supplier's end. Then another day suspecting your database layer. Then you finally pull out a hex editor and look at the raw bytes coming back from the API, and you discover the supplier's system is actually returning Latin-1 encoded content with a Content-Type: application/json header and no charset declaration. Your HTTP client, following the JSON spec, assumed UTF-8. The bytes got misread at the point of ingestion, and everything downstream inherited the corruption.

The fix is a one-line encoding declaration. The cleanup is a week of database migrations and reindexing.

Where These Bugs Hide

Encoding mismatches show up in places developers rarely think to check:

Database connections. Your database might be UTF-8, but if your connection string doesn't explicitly set the charset, some drivers will negotiate a different encoding during the handshake. Data goes in as UTF-8, gets stored differently, comes back mangled.

File I/O. Reading a file without specifying an encoding means you're at the mercy of the system default. On a Mac in development that might be UTF-8. On a Windows server in production it might be something else entirely. This is a classic works-on-my-machine situation with an encoding twist.

Email headers and bodies. SMTP is ancient and has its own encoding rules. Sending emails with non-ASCII characters without proper MIME encoding will result in subject lines and body text that look fine in your mail client during testing and show up as garbage for a meaningful percentage of your actual recipients.

Log aggregators. If your app logs UTF-8 and your log shipper or storage backend expects ASCII, those logs will either corrupt the non-ASCII characters or drop the entries entirely. Now you're debugging an incident with incomplete logs — which is its own special kind of nightmare.

How to Actually Defend Against This

You can't eliminate encoding bugs entirely, but you can make them loud and early instead of silent and late.

Be explicit everywhere. Don't rely on defaults. Set the charset on your database connections, declare encoding on every file operation, inspect the Content-Type headers on every API response you consume rather than just trusting the spec. Treat encoding as a first-class concern in your integration contracts.

Validate at the boundary. When data enters your system from any external source, validate that it's actually valid UTF-8 before letting it proceed. Most languages have utilities for this. A rejected request with a clear error message is infinitely better than corrupted data that propagates silently for three weeks.

Add encoding assertions to your tests. If you're writing integration tests against external APIs or file parsers, include strings with non-ASCII characters — accented letters, characters from non-Latin scripts, emoji. If your test suite only uses plain ASCII, you've got a blind spot the size of the international user base.

Audit your legacy systems. If you're running MySQL tables with latin1 collation, or you've got any component in your stack that predates the UTF-8 consensus, treat it as a live encoding hazard. Document the encoding expectations explicitly and convert when you can.

The Bug You Don't See Coming

Encoding bugs are a perfect example of what makes software infrastructure genuinely hard. The failure mode isn't dramatic. There's no exception, no crash, no obvious signal that something went wrong. There's just data that's slightly wrong, in a way that's easy to miss if you're not looking for it, spreading quietly through your system until it becomes impossible to ignore.

The lesson isn't that UTF-8 is broken or that encoding is impossibly complex. It's that assumptions about interoperability are liabilities, and the quieter the failure mode, the more dangerous the assumption. Every system boundary is a place where your encoding contract needs to be explicit, tested, and documented — not assumed.

Because the bug that doesn't crash your app is often the one that's hardest to find. And by the time you're staring at a database full of é where names used to be, you'll wish someone had just thrown an exception at the door.

All Articles

Related Articles

Nothing Changed and Everything Broke: The Dependency Update Trap

Nothing Changed and Everything Broke: The Dependency Update Trap

Your Server Is Slowly Eating Itself (And You Won't Notice Until It's Too Late)

When 0.1 + 0.2 Doesn't Equal 0.3: The Floating-Point Bug That's Already in Your Code

When 0.1 + 0.2 Doesn't Equal 0.3: The Floating-Point Bug That's Already in Your Code