A user signed up with an emoji in their name. Nothing broke for eight months. Then our export job started crashing every night at 3am.
Use full UTF-8 end to end and test with emoji and non-ASCII strings as normal QA. Bad encoding can sit silently for months before it surfaces.
correctnessdatabases
Problem
Our nightly customer export job started throwing encoding errors out of nowhere. No code had changed. The job had run cleanly for over a year before it began failing every single night at the same step.
Why it happens
- Emoji and other characters outside the Basic Multilingual Plane take 4 bytes in UTF-8, and one layer in the stack (a database column, an old library, a CSV writer) was still assuming everything fits in fewer bytes
- The bad data sits fine in the database for months because nothing forces it through the one code path that can’t handle it
- The eventual trigger is unrelated-looking: a new export job, a new integration, a migration, anything that finally reads and re-encodes that row
- By the time it breaks, the person debugging it has no idea a sign-up eight months ago is the cause, because the error surfaces somewhere completely different
Better approach
- Use full UTF-8 (
utf8mb4in MySQL, not plainutf8) end to end: database, application layer, and every export/import path - Test input handling with emoji, right-to-left text, and other non-ASCII strings as part of normal QA, not as an edge case you get to later
- Validate and normalize text encoding at the point of input, so bad data can’t sit silently for months before it surfaces
- When a job fails on “random” rows, check for encoding issues before assuming it’s a logic bug
Example
MySQL’s utf8 charset only supports up to 3 bytes per character, which covers most languages but not emoji. A signup form insert would silently truncate or error on save in a strict setup, but in a lenient one, it stores a mangled version that looks fine in the app and only reveals itself when a stricter downstream tool (like a CSV export library) tries to re-encode it and throws instead of guessing.