TLDR: Optimistic form dismissal feels fast. It also hides every failure. Await the server. Then dismiss.
The Setup
I'm building a native iOS CRM for tracking KOL relationships (Key Opinion Leaders: the physicians, researchers, and educators we collaborate with on medical webinars) — built in SwiftUI (Apple's declarative iOS framework).
Goal for the week: get it to TestFlight, Apple's beta distribution tool, so the team could actually use it.
The app talks to Supabase, our Postgres-backed database-as-a-service. Every contact field — bio, relationship owner, activity notes — gets written back to the database on save.
The Wall
The early save flow felt great.
Tap save. Form dismisses. Snappy, native, exactly what you want from iOS.
Except the data wasn't always actually saving.
Supabase was returning errors — nullable field mismatches, a CodingKeys conflict that ate exceptions whole, even a wrong argument order in the SupabaseClient initializer.
Real failures. But the form was long gone before the response ever came back.
Silent failures. The worst kind.
What I Tried First (Wasn't Enough)
I ran what I've been calling the WIN audit — went through every field in the contact model and made anything Supabase could return as null explicitly Optional in Swift.
That killed the decoder crashes.
I also untangled the CodingKeys conflict. Better.
But the root problem was still there: the form dismissed before it knew whether anything actually worked.
The Fix That Worked
One shift in thinking: don't dismiss until the server says yes.
In SwiftUI, concretely:
- Make the save function
async throws awaitthe Supabase upsert inside the button handler- On success →
dismiss() - On failure → set
errorMessage, stay on the form
do {
try await contactService.save(contact)
dismiss()
} catch {
self.errorMessage = error.localizedDescription
}
The form now sits there for the half-second it takes Supabase to confirm. And if something goes wrong — you see it, right there, before anything disappears.
That half-second isn't lag. It's a guarantee.
Why This Matters to Me
There's a principle I've had to re-learn more than once: if the user thinks data saved and it didn't, the bug isn't in the save — it's in the signal.
Optimistic dismissal looks responsive. But in a CRM where you're recording a conversation you just had with a physician… a silent failure is genuinely costly. That note is gone. The context is gone. You won't know until you open the contact three days later and it's blank.
"Feels fast" is not the same as "works."
Await the server. Then dismiss. Your users will never notice the difference — and they'll never lose their data either.