Shipping & Infra3 min read

One date column erased the whole screen

A region-comparison card vanished entirely. No error, the RPC returned 200, the data was there — the screen was just blank. A decoding failure on one date field I don't even display threw the whole decode, and the catch fell back to an empty array. No red light at any stage.

#ios#supabase#swift#gotchas
Concept diagram: server 200 → JSON "2026-07-01" → ISO8601 decoder throws for the whole row → catch returns an empty array → blank screen. Every earlier stage is a green check, only the last is an empty box.
A field you never display gets the power to kill the whole screen. Adding a field to the model is that dangerous.

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 screen

The 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 decoder

The 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()  // → first

App 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 swallowing decode failures 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?

Related