Skip to main content
Back to Blog
October 14, 2026 3 min read

The Bugs That Report Success

DebuggingPostgresEngineering
The Bugs That Report Success

Most bugs announce themselves. Something throws, a page goes blank, a test goes red, and somebody fixes it the same day. Those are not the expensive ones.

The expensive ones return 200. Here are two I have hit, from two very different stacks, and the thing they turned out to share.

One: the constraint nobody migrated

A public works map tracks road projects through a sequence of work activities. Someone added three new surface-treatment activity types to the admin UI. The dropdown offered them. Staff selected them. The page said saved.

They were not saved. The Postgres CHECK constraint on the activity table had never been migrated to allow the new values, so every insert was rejected. The reason nobody noticed for a while is the part worth dwelling on: the save path was a delete-then-reinsert routine, and its error branch called console.warn. The delete succeeded. The insert failed. The warning went to a console nobody had open, and the UI — which had been told nothing — reported success.

So the operation was worse than a no-op. It destroyed the previous rows and wrote nothing in their place, and told the user it had worked.

The fix was two changes, and only one of them was the constraint. The routine moved to an upsert, so there is no window where the old data is gone and the new data has not landed. And the error stopped being a warning and started being a failure the interface shows. The constraint migration fixed that Tuesday; surfacing the error is what stops the next one.

Two: the filter that matched everything

On an enterprise platform, email templates are scoped by region. A migration moved the filter from region name to region_id. The frontend kept sending the old query parameter.

PostgREST does not mind. An unrecognised query parameter is not an error — it is just not a filter. So the request succeeded, returned 200, and returned every region's templates. Marketing saw a longer list than they expected and had no particular reason to think the list was wrong.

There was no exception to catch, no log line to find, and no failing test, because the endpoint did exactly what it was asked. The only signal was a number that looked slightly too big to someone who knew what it should have been.

What they share

Both bugs live in the gap between the call completed and the thing you wanted happened. Every layer in both stories behaved correctly in isolation. Postgres correctly rejected a value that violated a constraint. PostgREST correctly ignored a parameter it was not configured to use. The application layer is the only place that knows what was actually intended, and in both cases it was not checking.

Three habits come out of this, and I apply them now without arguing with myself about it.

Never let a write path warn. If a write fails, something a person can see must change. A console.warn in a save routine is a decision to find out later, from a user, in worse circumstances.

Make destructive steps atomic. Delete-then-insert has a window where the data is gone. Upsert does not. The constraint bug would have been a visible failure rather than silent data loss if that window had never existed.

Assert the shape of what comes back, not just that it came back. A filtered query that returns more rows than the filter should allow is a failure, and it is cheap to check. "Did it 200?" is not the same question as "did it do the thing?", and only the second one matters.

None of this is clever. It is just taking seriously that the worst failure mode is not an error — it is confidence.

Available for work

Let's create your next big idea.