DEV Community

Cover image for How 'never' Type Prevents Broken Code in Production
Kelvyn Thai
Kelvyn Thai

Posted on

How 'never' Type Prevents Broken Code in Production

Most production bugs don’t come from syntax errors.
They come from valid code paths that nobody noticed.

TypeScript helps a lot — but there’s one class of bugs it does not catch by default, especially in React codebases that evolve over time.

This article shows:

  • a real React bug that silently ships to production
  • why TypeScript doesn’t warn you
  • how the never type turns that bug into a compile-time error

What Is the never Type?

In TypeScript, never represents something that cannot happen.

Not “returns nothing”.
Not “empty”.

But this:

A code path that is impossible to reach.

If a value has type never, it means:

  • there is no possible runtime value
  • if execution reaches here, something is wrong

Simple Examples

function crash(): never {
  throw new Error("Boom");
}
Enter fullscreen mode Exit fullscreen mode

This function never finishes normally.

function infiniteLoop(): never {
  while (true) {}
}
Enter fullscreen mode Exit fullscreen mode

Execution can never continue past it.

That’s why the return type is never.


The One Rule That Makes never Powerful

This rule is the key:

never is assignable to every type,
but no type is assignable to never (except never itself).

Example:

let x: never;

x = "hello"; // ❌ error
x = 123;     // ❌ error
Enter fullscreen mode Exit fullscreen mode

If TypeScript ever sees a real value being assigned to never,
it immediately fails the build.

We’ll use this rule to stop production bugs.


The Setup: A Common React Pattern

Imagine a component that renders UI blocks based on a type field
(CMS blocks, feature flags, workflows — very common).

type Block =
  | { type: "hero"; title: string }
  | { type: "cta"; text: string };
Enter fullscreen mode Exit fullscreen mode

Renderer:

function RenderBlock({ block }: { block: Block }) {
  switch (block.type) {
    case "hero":
      return <h1>{block.title}</h1>;

    case "cta":
      return <button>{block.text}</button>;
  }
}
Enter fullscreen mode Exit fullscreen mode

Everything looks fine:

  • TypeScript is happy
  • CI passes
  • No runtime error

The Production Bug (Silent)

A teammate adds a new block:

type Block =
  | { type: "hero"; title: string }
  | { type: "cta"; text: string }
  | { type: "banner"; image: string };
Enter fullscreen mode Exit fullscreen mode

They forget to update RenderBlock.

What happens?

  • No TypeScript error
  • No build failure
  • No runtime crash

The component returns undefined.
Nothing renders.

Production UI is broken silently.

This is the worst kind of bug.


Why TypeScript Didn’t Save You

From TypeScript’s point of view:

  • the function might return JSX.Element | undefined
  • that is technically valid
  • TypeScript does not assume your switch is exhaustive

So the bug passes.


The Wrong “Fix”

You might add a default:

default:
  return null;
Enter fullscreen mode Exit fullscreen mode

This makes things worse:

  • still no compiler error
  • still broken UI
  • now even harder to notice

We need TypeScript to fail loudly.


Introducing never

Now we change one line.

function RenderBlock({ block }: { block: Block }) {
  switch (block.type) {
    case "hero":
      return <h1>{block.title}</h1>;

    case "cta":
      return <button>{block.text}</button>;

    default:
      const _exhaustiveCheck: never = block;
      return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

*The result: *

This line is the key:

const _exhaustiveCheck: never = block;
Enter fullscreen mode Exit fullscreen mode

What Changed?

This line tells TypeScript:

“If block reaches here, this code is wrong.”

When banner exists, TypeScript now sees:

  • block can be { type: "banner" }
  • banner is not assignable to never

Compile-time error.
CI fails.
The bug never ships.


The Real Win

This is not about exhaustiveness checking.

It’s about this guarantee:

If the code compiles, all cases are handled.

That’s production safety.


Why This Matters in Real Codebases

This pattern shines when:

  • CMS schemas evolve
  • multiple teams touch the same union types
  • UI rendering depends on configs or API responses
  • reducers or workflows grow over time

Anywhere types change faster than implementations, never protects you.


Mental Model

never means:
“This code path must not exist.”

If TypeScript ever proves that it can exist,
your build fails — by design.


Conclusion

  • TypeScript does not guarantee exhaustive handling by default
  • React components can silently break in production
  • never turns missing cases into compiler errors
  • One line prevents an entire class of bugs
const _exhaustiveCheck: never = value;
Enter fullscreen mode Exit fullscreen mode

Use it where silence is dangerous.

Top comments (0)