
Jurij Tokarski
Three Ways the Wrong Value Won
A race condition, a stale default, and a spread operator each delivered the wrong value to production. None threw an error.
A user created a tender and immediately couldn't edit it. Not after a day, not after some permission change — immediately. They hit "Create," the page loaded, and the edit button was grayed out.
That was the first bug. It took three fixes across two projects before I understood what connected them: in each case, the value that reached the client wasn't the value I'd computed. Something else got there first — by being faster, by being stale, or by being last in the object literal.
The Value That Arrived Too Early
I pulled up the tender document in Firestore. The ai_driver field was missing entirely. The frontend created tenders like this:
const tenderData = {
title,
company_id: companyId,
...(companyData?.ai_driver && { ai_driver: companyData.ai_driver }),
};
New companies had no ai_driver set. The conditional spread evaluated to falsy, so the field was never written. That was supposed to be fine — a Cloud Function trigger would set the default after creation.
The Firestore snapshot listener had other plans. It fired before the Cloud Function, saw no ai_driver, and ran this check:
const isDiscontinuedDriver =
!tender.ai_driver || DISCONTINUED_AI_DRIVERS.includes(tender.ai_driver);
Missing field. Falsy. "Discontinued." Read-only. The user just watched their tender lock itself. Every single tender created by a new company since this code shipped had been born locked.
The fix had two parts. The frontend writes every field it reads immediately after creation — no delegating defaults to triggers:
const tenderData = {
title,
company_id: companyId,
ai_driver: companyData?.ai_driver || DEFAULT_AI_DRIVER,
};
And the discontinuation check had to distinguish "missing" from "actively deprecated":
const isDiscontinuedDriver =
tender.ai_driver && DISCONTINUED_AI_DRIVERS.includes(tender.ai_driver);
Deployed both. Bug reports kept coming.
The Value That Outlived Its Meaning
Different users, same symptom. Tenders locked on creation. But these companies had ai_driver explicitly set in Firestore — set to assistants-api-gpt4o, a driver I'd discontinued months earlier.
I traced it to the organization settings form:
aiDriver: company.ai_driver || "assistants-api-gpt4o",
That hardcoded fallback was a leftover from migration. New companies had no ai_driver in Firestore, so the form loaded with a dead value nobody could see. The field wasn't even visible on the settings page — it was an internal config, not a user-facing dropdown.
The form submitted its entire state on every save. A user enables a jurisdiction toggle, hits save, and the payload includes ai_driver: "assistants-api-gpt4o". The backend guard:
if (payload.ai_driver) {
update.ai_driver = payload.ai_driver;
}
Truthy string passes. The discontinued driver gets written to Firestore. Every tender created after that inherits it. The user who toggled a jurisdiction setting three weeks ago has no idea they just broke tender creation for their entire organization.
I dropped the hardcoded fallback. Deployed. Reports kept coming — users had the old bundle cached. Every save from a cached session re-wrote the stale value, undoing any Firestore cleanup I ran manually.
The frontend fix wasn't the real fix. The real fix was backend enum validation:
if (payload.ai_driver && Object.values(AIDriver).includes(payload.ai_driver)) {
update.ai_driver = payload.ai_driver;
}
The backend rejects any value not in the current enum. Cached bundles, stale defaults, garbage input — all dropped. The frontend can send whatever it wants; the backend is the last line, and it has to act like it.
That stopped the bleeding. But the pattern was already in my head when I opened a different codebase weeks later.
The Value That Was Always Last
I was reviewing a feature flag called ai_chat_enabled. The backend computed it from the user's subscription plan — a careful if/else chain that looked up the plan, checked edge cases, and resolved to a boolean. Solid logic. Well-tested in isolation.
Then I looked at the response builder:
return {
statusCode: 200,
body: {
email: email_address,
name: name,
plan: plan,
ai_chat_enabled: ai_chat_enabled,
...customerPreferences,
},
};
customerPreferences came from DynamoDB. It contained its own ai_chat_enabled key — the raw stored preference, not the computed one. The spread came after the explicit assignment.
JavaScript object literals follow last-writer-wins. The spread silently overwrote the computed value with whatever was sitting in the database. The entire plan-based computation — the lookup, the edge cases, the if/else chain — never reached the client. Not once. Not since the day this code shipped.
The tests checked that the computation logic returned the right boolean. They never checked that the response builder actually used it.
The fix was one line — move the spread before the explicit fields:
return {
statusCode: 200,
body: {
...customerPreferences,
email: email_address,
name: name,
plan: plan,
ai_chat_enabled: ai_chat_enabled,
},
};
Computed values last. Raw data first. The spread provides defaults; the explicit fields override them.
The Wrong Value Always Has a Way In
Timing, staleness, ordering. Three mechanisms, same result: the value I intended never made it. If the frontend reads a field, the backend must validate it. If the backend computes a value, nothing downstream should be able to quietly replace it. The wrong value will always find a way in. The only defense is making sure the right value goes last.
Subscribe to the newsletter:
About Jurij Tokarski
Hey 👋 I'm Jurij. I run Varstatt and create software. Usually, I'm deep in the work shipping for clients or building for myself. Sometimes, I share bits I don't want to forget: mostly about software, products and self-employment.
x.comlinkedin.commedium.comdev.tohashnode.devjurij@varstatt.comRSS