← all articles

A Server Can Say More Than The Client Can Hear

A Server Can Say More Than The Client Can Hear

If a server decides what happens next in a flow, then every value it can return is a promise that each client knows what to do with it. Break that promise and the failure shows up on the one device that cannot explain it. The cheapest way to keep the promise is a test that lists everything the server can say and fails the build when any client has no answer for one of them.

I was reminded of this by a bug in an onboarding flow. The server walks a new user through a series of questions and tells the app which one comes next. Someone added two new questions on the server side. The web app got screens for both. The mobile app did not. Nobody decided to leave mobile out. The change simply never reached it, because nothing connected "the server can now return this" to "the phone needs a page for it".

The failure that leaves no trace

On a phone, a person answered the hobbies question, tapped Continue, and got a generic "something went wrong". Closing the app and reopening it landed them on the same error. That second detail matters. The server had stored their progress, so every launch asked it where they were up to, and it cheerfully replied with the new question the app could not display.

Underneath, the mobile app looked up the next step in a table of routes, found nothing, and threw a lookup exception. The screen caught it and showed the only message it had. No request failed. The server did exactly what it was designed to do, so its logs were clean. If you had gone looking on the server side, you would have found a healthy system and a confused customer.

That is the uncomfortable shape of this class of bug. The component that is wrong is not the one reporting the symptom, and the component reporting the symptom has no vocabulary for what went wrong. A person in the first minutes of using something new has just been told it is broken, in words that tell them nothing, at the exact point where they were deciding whether to bother.

Why it happens

It is not carelessness. When a server owns the sequence, the sequence is invisible from the client's side until it changes. Adding a step feels like a server change with a web change attached, because the web screens are in the same repository, or the same mental neighbourhood, or the same week. The mobile app ships through a separate pipeline, often on a separate cadence, and gets forgotten for entirely ordinary reasons.

There is also a quiet commercial pressure. The people who asked for the new questions wanted them in front of customers, and the web is where the quickest feedback lives. Nobody is being negligent in choosing to ship there first. The trouble starts when "first" silently becomes "only" and the server has no idea.

Make the gap a build failure

The fix for the people affected was simple: build the two missing screens, match the wording and behaviour of the web versions, register the routes. Anyone stuck simply carries on from the new question once they update. That repairs this instance.

The more valuable change was the test. It takes every step the server is able to return and checks that the mobile app has a route for each one. Now a future step added without a mobile screen fails in the pipeline rather than in someone's hand. The test costs almost nothing to write and removes a whole category of "we forgot a client".

I like this kind of check because it turns an unwritten agreement into something that can be wrong out loud. The agreement was always there: the server will not say anything the client cannot hear. It just lived in people's heads, which is a poor place to keep a promise between two codebases.

What the client should do when it cannot hear

A test only protects you at build time. Real devices run old versions for weeks, and sooner or later a client will meet a step it has never heard of. I have come to prefer clients that treat an unknown step as a situation to be handled rather than an exception to be survived. That might mean offering to skip an optional question, or telling the person plainly that the app needs updating. Either is better than a generic apology that implies they did something wrong.

Whether the client should be allowed to skip is a product call, and it depends on what the step is for. An optional question about someone's job is very different from a consent step. Letting a client wave through something it does not understand would be a mistake where the answer matters. In those cases the honest behaviour is to stop, say why, and not pretend.

The principle underneath

Whoever can change the conversation has to account for everyone who is listening. A server that drives the flow is making a statement every time it replies, and "I can't tell what you meant" is a terrible thing to receive from software at the start of a relationship. Keep a list of what you can say, keep a list of what each client can hear, and make the build compare them. It is dull work, and it is exactly the kind of dull that stops a customer's first impression from being an error message.

Matthew Ratcliffe, software developer and architect, Ballarat
Senior Software Engineer & Architect

20+ years across the technology stack — from greenfield builds to brownfield rescues. Based in Ballarat, VIC, focused on AI, healthcare and high-risk data systems. Full resume →

Share your thoughts