A status the page never learned
The tasks page went white the day a triage used a status value the client had never been told about.
By Andrew Pyle
The bug shipped on a Friday and waited ten days for a row that would set it off.
01A blank page
Cannot read properties of undefined
TypeError: Cannot read properties of undefined (reading 'type')
That was the whole message, thrown from inside the minified chunk of the component library, and the tasks page rendered blank. I found it the ordinary way, by opening the page and seeing nothing on it. No monitor and no test had caught it first.
Production ships without source maps, so the stack frame pointed at minified code I could not map back to a line. The way through was to read the page source and look for a pattern I had met before: a call to the library's alpha() color helper, fed by a lookup into a map keyed on a status or type.
02Ten days early
The cause shipped well before the crash
On October 2 the server gained a new task status, superseded. It came with a reason field, a link to the task that replaced it, and a note. It was also terminal. I added it to the set the serializer uses so that a superseded task cannot be reopened.
Nothing broke that day, or for the next ten. On October 12 a backlog triage superseded 132 duplicate tasks in one bulk pass, and that was the first time real rows holding the value reached the page. Until then, no code path had ever asked the client what color superseded should be.
03Undefined, then type
One lookup returned nothing
The task list draws a colored chip for each task's status, and the color comes from a map of statuses to colors. Superseded had no entry, so the lookup returned undefined. The chip passed that into alpha(), which begins by decomposing the color, and the first thing that does is check color.type. Reading type off undefined is the exact error in the console.
The type system helped only partway. The color maps are typed as a record over the status union, so once a status is in the union, the compiler forces every map to carry a key for it. The server's new value had never been added to the client's union, so the compiler had nothing to enforce. It cannot see a value the server sends that the client has never declared.
04The second time
I had already written this lesson down
This was the second white screen of this kind in the same app. On September 11 a dependencies page crashed the same way. Its frontend type and the backend's choices disagreed on almost every value:
frontend DependencyType: import, api_call, data_flow, event
backend choices: imports, api_call, database, message, config, integrationOnly api_call matched both, so any other row broke the page through the same alpha() call.
After that one I wrote down two remedies. Guard every map lookup that feeds alpha() with a fallback. Build a contract test that fails when the frontend union does not cover the backend's choices. The note marked the contract test as not yet built. A month later it still was not, and the tasks page found the same gap.
05A dim chip
An unknown status now renders gray
The fix was about 15 lines across three files. Superseded went into the client's status type and its list of terminal statuses. It got a label, the same dim color as cancelled, a place in the status filter, and a slot in the chart order between done and cancelled.
The structural fix is one line in the chip itself. Its color is now allowed to be undefined, and an undefined color falls back to the dim neutral:
// An unknown status must not crash the page: MUI alpha() throws on undefined.
const color = rawColor ?? commandColors.dim;The next status the server adds before the client learns it will show up as a plain gray chip.
The change lists what I did not check. The page and shared tests passed, 11 of 11. The type check showed only errors that were already there from local modules. I did not add a render test with a superseded row, and I did not open it in a browser before merging. The check after deploy is plain: open the page, confirm it renders, confirm the filter lists Superseded.
The contract test is still unbuilt. The fallback keeps the page standing, and it also changes how the next drift will look. The crash was loud enough that opening the page was all it took to find it. A gray chip for a status the client does not know is quiet, and quiet drift is easy to live with for a long time.
Related
writing
A limit that was never enforced
Eighty of my scheduled tasks ran on a time limit different from the one they declared, because the loader quietly threw it away, and the first plan I built on those numbers was wrong.
writing
A deploy should drain, not kill
A routine deploy quietly ate a day of work, and the monitor that was supposed to notice reported nothing wrong.
writing
A cold cache is not free
My feature-flags page felt instant once I cached it, and the first request to each fresh worker after a deploy still paid about fifteen seconds.
writing
A 200 is not the same as access
My leak probe failed with seventeen leaks on its first real run. Every one of them was the same page that was never a leak.
writing
The animation that never ran
Every build check I ran came back green, and the motion on the live page sat frozen.