Solo dev shipping 10 UI languages: what silently broke?

by•

Post-mortem from last week.

My site is translated into 10 languages. The language switcher and the internal links on every localised page were supposed to keep visitors in their language. For months they did not. Every locale page quietly linked back to the English homepage.

The cause was one line. A lookup used slashless keys like /for/bakeries while the site's own canonical path helper appended a slash. Every lookup missed, returned an empty array, and nothing threw or logged. 376 pages shipped like that.

The fix was one line, in two files, because the manifest generator and the build were separate steps.

Lesson I took: a function that returns empty instead of throwing will hide a bug for as long as you let it.

What is the silent bug that lived longest in your product before you noticed?

2 views

Add a comment

Replies

Best

Yours has a sharper cousin. A lookup that returns empty is bad; a write that matches nothing is worse, because an empty result from a delete looks exactly like success.

The one I watch most closely in my invoicing app is sign out everywhere. Changing a password deletes every session the user has. The session library stores its payload as an opaque blob with no way to query inside it, so the delete matches rows by the exact encoded text it expects the library to have written, a single user id key. If that library ever writes a second key, the delete matches zero rows, returns zero, and a password reset stops evicting the intruder it was performed to evict. Nothing throws. The page still says the password was changed.

The guard is a test that creates a session through the real library and then asserts my query finds it. It asserts the positive on purpose. A test that only checked the delete ran without error would pass on the broken version, because zero rows deleted is not an error.

The second one I caught while writing, not in production. My query builder accepts a list filter in two spellings, and the short one binds the whole list as a single value. It matched nothing and reported success, so a delete written that way would have removed zero rows. What caught it was a test that counted the rows left behind rather than trusting the call.

A third lived about two weeks: a primary key declared on the wrong field of the user table. Nothing failed, because exactly one thing read that declaration, the schema generator, and it only objected the next time I asked it for a migration, with an error that named no table.

Your one line in two files is the part I would keep. Wherever the shape of a key is decided twice, the expected shape now lives in one function that both the code and its test call, so there is one place left to drift.