DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Our 404 is a field guide entry for a bird that was never recorded, and our error boundary is not allowed to be funny

Nakodo is a creator outreach app with a field guide for a brand: a bird called Alcy, specimen labels, Latin names, catalogue numbers. The question that comes with a brand like that is where it is allowed to speak. We answered it in the brand document before building any of these screens, in one line copied off Sentry and PostHog:

Jokes in the waiting room, never at the counter. Loading, empty, done, the 404, footnotes and changelog titles. Instructions, errors about money and billing stay literal.

This post is the two halves of that rule as code, plus a thing I found about our own 404 while writing it.

The waiting room: a 404 written as a specimen that was never collected

The brand document specifies the 404 as copy, before any markup:

No. 404

Page not recorded in this area

Pagina absens

Field marks: none. Range: unknown. Last seen: never.

The link may be old, or the address mistyped.

Which comes out as src/app/not-found.tsx, a page with one comment in it:

// The one place the field guide parodies itself.
export default function NotFound() {
  return (
    <div className="flex min-h-svh flex-col">
      <header>{/* logo */}</header>
      <main className="mx-auto flex w-full max-w-5xl flex-1 flex-col items-start justify-center gap-10 px-5 pb-24 sm:flex-row sm:items-center sm:px-8">
        <MissingSpecimen size="l" className="shrink-0" />
        <div className="max-w-md space-y-4">
          <p className="catalogue text-muted-foreground">No. 404</p>
          <h1 className="text-[32px] leading-10">Page not recorded in this area</h1>
          <p className="font-display text-xl text-muted-foreground italic">Pagina absens</p>
          <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 border-y py-3 text-sm">
            <dt className="label-caps pt-0.5 text-muted-foreground">Field marks</dt>
            <dd>None</dd>
            <dt className="label-caps pt-0.5 text-muted-foreground">Range</dt>
            <dd>Unknown</dd>
            <dt className="label-caps pt-0.5 text-muted-foreground">Last seen</dt>
            <dd>Never</dd>
          </dl>
          <p className="text-muted-foreground">The link may be old, or the address mistyped.</p>
Enter fullscreen mode Exit fullscreen mode

The joke is structural rather than written: it is a real field guide entry, in the same dl layout the app uses for a creator's facts, filled in for something that does not exist. Nobody has to find it funny for the page to work, which is the test I apply to all of this. "Pagina absens" is the only line that would be noise if it were not immediately followed by a plain sentence about old links and typos.

The <dl> matters more than it looks. Three label and value pairs are a description list, not a table and not six <p>s, so a screen reader reads "Field marks, none" as a pair.

The illustration is one of twelve, and it is drawn as an absence

Illustrations in this codebase are not chosen by whoever builds a screen. There is a numbered library in the brand document, and number 12 is this one:

| 12 | missing-specimen | A dashed-outline bird silhouette on an empty mount, with a catalogue label "No. 404" | 404 page, deleted creator | M |

The implementation has one trick in it worth stealing. Alcy is drawn as four overlapping paths: tail, body, bill, head. Stroking those with a dash pattern would dash every internal seam where the paths overlap, which looks like a broken drawing rather than an absent bird. What is wanted is the dashed outline of the union of the four shapes, and computing a union of bezier paths in the browser is not something I was going to do:

const SILHOUETTE = [ALCY.tail, ALCY.body, ALCY.bill, "M28 29 a13 13 0 1 0 26 0 a13 13 0 1 0 -26 0 Z"];

/** 12. missing-specimen: a dashed bird silhouette on an empty mount, labelled "No. 404". */
<g transform="translate(0 -4)">
  <g strokeWidth="3" strokeDasharray="2 6">
    {SILHOUETTE.map((d) => <path key={d} d={d} />)}
  </g>
  <g fill={FILL} stroke="none">
    {SILHOUETTE.map((d) => <path key={d} d={d} />)}
  </g>
  <path d={ALCY.feet} strokeDasharray="2 3.5" />
</g>
Enter fullscreen mode Exit fullscreen mode

Draw every path twice. The first pass strokes them at double the normal width, 3 instead of the system's 1.5. The second pass fills the same paths with the page colour and no stroke, which covers the inner half of every stroke and all of the internal seams. What survives is the outer half of the outermost stroke: a 1.5 wide dashed line exactly on the silhouette's edge, matching every other illustration's line weight, with no union computed anywhere.

Dashed is not a style choice either. The system assigns meanings: hatching means "not yet observed" or "locked", and dashed outlines mean "absent". A dashed bird on an empty mount is the same vocabulary a locked tag uses elsewhere in the app, which is why it reads without a caption.

Under the bird there is a perch, a post and a plinth, and the label "No. 404" set in the monospace face. The plinth is empty. That is the whole gag, and it took 49 lines.

The counter: the error boundary gets no illustration and no voice

The app's error boundary is three quarters of a screen shorter, and the comment is the design:

"use client";

// Errors are plain: no illustration, no joke.
export default function AppError({ error, retry }: { error: Error & { digest?: string }; retry: () => void }) {
  useEffect(() => {
    console.error(error);
  }, [error]);
  return (
    <EmptyState
      title="This page didn't load."
      actions={<Button variant="outline" onClick={() => retry()}>Try again</Button>}
    >
      Your campaigns and results are saved. If it keeps happening, try again in a few minutes.
      {error.digest && <span className="mt-2 block font-mono text-xs">Reference {error.digest}</span>}
    </EmptyState>
  );
}
Enter fullscreen mode Exit fullscreen mode

Everything in there is the rule being obeyed.

EmptyState takes an optional illustration prop, and of its twenty call sites this is the only one that leaves it out, because a mascot next to a failure is the app being pleased with itself while the customer is not. The body answers the question somebody actually has, which is not "what went wrong" but "did I lose anything", and Nakodo is an app that sends email on a brand's behalf, so "your campaigns and results are saved" is the only sentence worth the space. error.digest is printed because it is the one string a person can give us that lets us find their stack trace in the logs, and it is set in production only.

A 404 means somebody typed a URL wrong. An error means our code broke in a product that spends a brand's money and writes in their name. Those deserve different registers, and writing the rule down first is what stopped me from giving them the same one.

The part I only found by curling it

Here is the thing. Try the 404 yourself and it works:

$ curl -s -o /dev/null -w '%{http_code}\n' https://nakodo.app/guides/nope
404
Enter fullscreen mode Exit fullscreen mode

That response is the specimen page, with <title>Page not found ยท Nakodo</title> and <meta name="robots" content="noindex"/> in the head, which Next adds to a not-found render without being asked.

Now try a path that is not under one of the public families:

$ curl -sI https://nakodo.app/no-such-page | grep -i '^http\|^location'
HTTP/2 307
location: /login?next=%2Fno-such-page
Enter fullscreen mode Exit fullscreen mode

Our proxy allowlists the public paths and sends everything else to sign-in, and that includes paths that do not exist at all. A signed-out visitor mistyping nakodo.app/pricingg does not meet the bird: they meet a login form with ?next=/pricingg in it, and after signing in they get the 404 they were owed in the first place.

The 404 is therefore reachable to the public only under the prefixes the content lives at: /guides/, /blog/, /tools/, /youtubers/, /tiktokers/, /instagrammers/, /for/, /alternatives/, /og/, /o/. Those are the paths an old or broken link is actually likely to use, which is the saving grace, and it is luck rather than design.

I am not going to pretend this is fine. Two things are wrong with it:

The allowlist cannot distinguish "needs a session" from "does not exist", because it runs before routing. Fixing it properly means telling the proxy which paths are real, which is the registry it already has for content pages but not for everything else, and I would rather have a login redirect than a second source of truth about what exists.

And the 404's own buttons are wrong for a stranger. The brand document specifies "Go to campaigns" and "Go back"; the page ships "Go to campaigns" and "Home page", since going back needs client-side history and a link does not. Either way the primary action on a page a signed-out reader reaches from a stale link in a guide is a button that redirects them to a sign-in form. The sensible version swaps the order when there is no session, or points at the guides index instead, which is where they were heading. That is a small change, and it is on the list now because a terminal told me something the app never would have.

The rule earns its keep

The useful part of "jokes in the waiting room, never at the counter" is not the jokes. It is that it decides in advance, and the decision is checkable in review: this screen is a counter, so delete the bird. Without it, every empty state is a conversation about tone, and the conversation is louder for whoever is most tired that week.

Two live pages to compare, if you want the two registers side by side: the home page is the waiting room, with sample specimen entries, four illustrations and the bird standing on the edge of the hero plate, and the pricing page is nearly the counter, with one illustration beside the plans and nothing but plain numbers in the table under them.

Top comments (0)