A region-comparison card vanished from the screen entirely. No error message. The RPC was returning fine and the server logs were clean. The card was just gone.
No red light at any stage
A Postgres date column comes down from PostgREST as "2026-07-01" — no time. Receive that into a Swift model as let basisYmd: Date? and supabase-swift's ISO8601 decoder rejects it, because there's a date but no time.
So far it's a common type mismatch. The problem is what comes next.
A decoding failure on one field throws the entire decode. And most code falls back to an empty array in catch. An empty array makes the screen decide "there's no data" and draw nothing. It looks like a normal empty state, not an error.
So the symptom is this — the server returns 200, the data is actually there, the network tab shows nothing wrong, and only the screen is blank. The whole screen disappeared because of one field it doesn't even display.
When all-or-nothing decoding meets a catch that falls back to empty, even an unused field gets the authority to kill everything. Which means adding a single field to the model is that dangerous.
The sibling trap — .single()'s 406
The same library has a sibling trap. .single() throws a 406 if the result isn't exactly one row. There's no maybeSingle like other SDKs have, so using .single() on a query that might return zero rows turns a normal "not found" into an HTTP error. Use .limit(1) then first. Both belong to the same family: "fails silently, or in the wrong shape."
Code
The problem:
struct RegionRow: Decodable {
let basisYmd: Date? // ← Postgres date column. "2026-07-01" arrives
// ...
}
// ISO8601 decoder rejects → whole decode throws → catch → [] → blank screenThe fixes:
// 1) if you don't display the date, take it as String (simplest)
let basisYmd: String?
// 2) if you genuinely need a Date, add a date-only format to a custom decoderThe sibling trap:
// ❌ 406 if zero rows
try await client.from("t").select().eq("id", id).single().execute()
// ✅
try await client.from("t").select().eq("id", id).limit(1).execute() // → firstApp in this story: HiddenGem.
Honestly
- Finding the cause took a while. The fact that the RPC was responding fine actually drove the investigation the wrong way. If the server is fine, you suspect client logic — but the real culprit was the decoding layer in between, and it had no logs.
- I didn't fix the root. I just changed a date I don't display to
String?. I can make the same mistake next time. Doing it properly means adding a date-only format to a custom decoder, or not swallowingdecodefailures into an empty array in the first place. The latter is the real fix, and I haven't done it. - The same project also had a case of timestamptz's 6-digit microseconds being rejected. I got caught by this library's date handling three times — the result of patching symptomatically each time.
What makes this bug scary is that "a catch that swallows failures" and "all-or-nothing decoding" are each reasonable on their own. The problem is only where they overlap. Picture one catch { return [] } in your code. Is it really swallowing an "empty result" — or a decode failure on one field you don't even display?