Back to News & Insights
SEO September 6, 2026 · 4 min read

Your Flutter 404 Page Is Probably Crashing, and Your Server Is Probably Lying About It

go_router's errorBuilder renders outside the route subtree, so GoRouterState.of throws there — and there is no maybeOf.

Your Flutter 404 Page Is Probably Crashing, and Your Server Is Probably Lying About It

Someone sent me a screenshot of my own 404 page. Washed-out grey text on a light background, barely readable, nothing like the dark theme every other page uses. My first thought was a styling bug.

It was not a styling bug. The page was crashing before it could paint, and the fallback I had built for exactly that case was doing its job. Underneath it was a second bug that had been hiding the first one for months.

The site is a Flutter web app with every route prerendered to a real HTML file. Firebase Hosting served those files, and anything unmatched hit a catch-all rewrite:

That looks right. It is not, and the reason is worth internalising: a Firebase rewrite always responds 200. That is what a rewrite is — serve this other content under the requested URL. So every nonexistent URL on the site returned 200 OK with a page that said "Page not found".

Browsers do not care. Crawlers care a great deal. A 200 means "this is a real page, index it", so every typo, every dead inbound link, every scanner probing for /wp-admin was eligible to be indexed as a real page. This is the soft 404, and it is invisible from a browser because the page looks correct.

The fix is to delete the catch-all rather than repoint it. With every real route prerendered as a file, Firebase serves those directly, and for anything with no matching file it falls through to its own handling — which serves 404.html with an actual 404 status.

One caveat that will bite you if your app has client-only routes. My admin panel has no prerendered file, so removing the catch-all 404'd my own admin URL. It needs a rewrite scoped to that path alone:

Scoped rewrites for the routes that genuinely need them; no catch-all; real 404s for everything else.

Fixing the status code is what made me actually look at the page, and that is when the grey rendering stopped looking like a theme problem.

My prerendered HTML carries the page's text in the DOM, clipped to a single pixel, with a boot guard that reveals it if Flutter never paints. The idea is that a failed boot shows the content rather than a spinner turning forever. So the washed-out text was the guard firing correctly, telling me Flutter had died.

Here is the chain. errorBuilder renders my NotFoundPage. That page uses the same shell as every other page, and the shell contains the nav bar, and the nav bar asks which route is current so it can highlight the right link:

On every real route that is fine. On the 404 page it throws, because errorBuilder renders outside any RouteBase.builder — there is no route subtree above it and therefore no GoRouterState to inherit.

The instinct is maybeOf. In gorouter 17 there is no GoRouterState.maybeOf. of() throws unconditionally; there is no nullable variant to fall back to.

So the next instinct is to ask the router itself, which definitely sits above everything:

I tried exactly this, and it fails too — differently, which is what makes it interesting. GoRouter.state reads matches.last on the current match list, and on an unmatched URL that list is empty, so you get a StateError: Bad state: No element. The accessor fails for precisely the reason you are on the 404 page at all.

Both router-side answers are dead ends. But the browser knows the path regardless of what the router made of it:

The broad catch is deliberate, and I would defend it specifically here. Determining which nav item to highlight is decoration. This widget renders inside every page on the site. Failing at decoration must never be able to take down the page around it, and the two failure modes above are different exception types from different call sites — narrowing the catch buys nothing except a chance to miss the third one.

Nothing highlights on a 404, which is correct: no nav item corresponds to a page that does not exist.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation