Writing

The page that blamed the browser

28 October 2026

An exchange started from the front page reached its order page and showed nothing. Not an error we wrote, not a stuck spinner — a blank page with a line of apology on it. The order itself was real: it had been created, the provider had it, the deposit address existed. Only the page describing it was gone.

It was blamed on the browser four times before anyone looked properly.

Everything that was not wrong

The first theories were all about the visitor’s machine. A content blocker eating a provider’s name. A service worker serving something stale. A proxy in front of the site. Disk pressure. A renderer that had run out of memory. Each one was plausible, each one was cheap to believe, and each one put the fault somewhere nobody could inspect.

Then the network tab settled it. Thirty-one requests, every one of them 200 — the document, the scripts, the manifest, the data payload, the fee and price calls, the currency lists. 208 kB transferred. Nothing blocked, nothing refused, nothing missing.

The console had one line in it:

Uncaught TypeError: Cannot read properties of undefined (reading 'toUpperCase')

The page had loaded completely and then destroyed itself. A throw during render unmounts the tree, and what replaces it is a blank frame with a sentence on it. That blank frame is not the browser failing to load a page. It is a page that loaded perfectly and then decided to stop existing — and it looks identical from the outside, which is why four wrong theories survived as long as they did.

The answer the provider gives, and the question it was never asked

Creating an exchange means telling a provider a pair, an amount and an address, and getting back what the provider decided: an identifier, a deposit address, an expected payout. That answer was stored as the order.

It carries nothing about the pair. The provider was never asked what the coins were — it was told, by the page, a moment earlier. So the currencies, the chains and the amount came back undefined, were saved as the order, and the first attempt to put a missing ticker into capitals threw.

Two things made it survive longer than it should have. It only failed on real orders, because every order made by hand for testing had all its fields filled in. And the compiler had no objection: the answer arrives as any, and assigning any to a type whose fields are all required type-checks perfectly. Four local builds passed. So did the type check. So did a boot test.

Two lessons, and only one of them is about code

The code lesson is small and general: build an object field by field against the type it claims to be, rather than handing it something shaped like an answer to a different question. Spreading an API response into a typed slot is a promise the compiler cannot check and will not warn about.

The other lesson is the expensive one. When something fails in a way you cannot reproduce, there is a strong pull toward explanations that live outside your own code — the network, the browser, the extension, the user’s machine. Those explanations are comfortable precisely because they cannot be tested, which is the same reason they are worthless. Every hour spent on them was an hour not spent reading the one line in the console that said exactly what had happened.

The page was not failing to load. It was loading, and then throwing. Those two things look the same and have nothing in common.

Plan an exchange

Every figure above can be checked against the providers yourself — no account, no sign-up, and nothing is held for you at any point.