The never type in TypeScript is often explained as “a function that never returns”.
While technically correct, this explanation is rarely useful in real-world codebases.
In practice, never becomes powerful when you want the compiler to fail loudly as your
application evolves — especially when handling errors, exhaustive checks, and states
that should be impossible but tend to appear over time.
In this guide, we’ll look at how never behaves in production code, why it is easy
to misuse, and how it helps catch entire classes of bugs before they reach runtime.
What never Really Means
The TypeScript type system models types as sets of values:
-
numberis the set of all possible numbers -
booleanis the set{true, false} - string literal types like
"ready"are a set with a single element -
neveris the empty set
Because never has no possible values, no actual value can ever belong to it. That’s why the following function is valid:
export function triggerFailure(reason: string): never {
throw new Error(reason);
}TypeScript ensures that functions returning never cannot complete normally.
const outcome = triggerFailure("Unexpected state!");
// ^? neveroutcome has type never, because TypeScript knows the function cannot return.
Exhaustive error handling
type ApiResult =
| { status: "success"; data: string }
| { status: "error"; error: Error };
function assertNever(value: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(value)}`);
}
function handleResult(result: ApiResult) {
switch (result.status) {
case "success":
return result.data;
case "error":
throw result.error;
default:
return assertNever(result);
}
}Here, never ensures that every possible state of ApiResult is handled. If a new status is added later, TypeScript will immediately report an error.
This is where never becomes valuable — not as a theoretical type, but as a guardrail for evolving codebases.
The Common Mistake: Using never for Error Modeling
Developers sometimes attempt this pattern:
export function safeDivide(x: number, y: number): number | never {
if (y === 0) {
throw new Error("Division by zero!");
}
return x / y;
}They expect the return type to reflect both “good” and “error” states.
But TypeScript evaluates:
number | never → numberThe empty set adds nothing to the union — never collapses and becomes meaningless.
This is why:
const result = safeDivide(10, 0);
// result: numberThis code successfully compiles but is misleading.
The never type should never be used to describe an error state.
Why never Collapses in Unions
Unions represent the set‑theoretic union of their members.
(number-set) ∪ (empty-set) = number-setThis is not a TypeScript quirk — it is mathematically correct.
Thus:
-
string | never→string -
boolean | never→boolean -
T | never→T
never disappears because it cannot contribute any valid value.
The Correct Use of never: Exhaustiveness Checking
never becomes powerful when used to model impossible states.
Step 1 — Create a discriminated union
type CircleShape = { kind: "circle"; radius: number };
type SquareShape = { kind: "square"; side: number };
type RectShape = { kind: "rectangle"; width: number; height: number };
type ShapeEntity = CircleShape | SquareShape | RectShape;Step 2 — Write an exhaustiveness checker
function assertImpossible(value: never): never {
throw new Error("Encountered an impossible case: " + JSON.stringify(value));
}Step 3 — Use it inside a switch
export function computeArea(geom: ShapeEntity): number {
switch (geom.kind) {
case "circle":
return Math.PI * geom.radius ** 2;
case "square":
return geom.side ** 2;
case "rectangle":
return geom.height * geom.width;
default:
return assertImpossible(geom);
}
}If someone later updates:
type TriangleShape = { kind: "triangle"; a: number; b: number; c: number };
type ShapeEntity = CircleShape | SquareShape | RectShape | TriangleShape;The compiler immediately screams:
Argument of type 'TriangleShape' is not assignable to parameter of type 'never'.This is exactly how never should be used:
catching missing branches
preventing silent logic failures
enforcing total coverage of all cases
A Better Model for Error Handling: The Result Pattern
Instead of misusing never, model error states explicitly using a discriminated union:
type FailureInfo = { status: "fail"; message: string };
type SuccessInfo<T> = { status: "ok"; data: T };
type ResultBox<T> = FailureInfo | SuccessInfo<T>;Helper creators:
const createFailure = (msg: string): FailureInfo => ({
status: "fail",
message: msg,
});
const createSuccess = <T>(value: T): SuccessInfo<T> => ({
status: "ok",
data: value,
});A safe division using proper error modeling:
export function robustDivide(n1: number, n2: number): ResultBox<number> {
if (n2 === 0) return createFailure("Division by zero");
return createSuccess(n1 / n2);
}Usage:
const output = robustDivide(8, 0);
if (output.status === "fail") {
console.error("Error:", output.message);
} else {
console.log("Result:", output.data);
}This is explicit, predictable, and fully typed.
Wrapping Unsafe Functions: A try-Safe Wrapper
Sometimes an existing function throws. Wrap it safely:
export function handleSafely<Args extends unknown[], Ret>(
fn: (...p: Args) => Ret,
...inputs: Args
): ResultBox<Ret> {
try {
return createSuccess(fn(...inputs));
} catch (err: any) {
return createFailure(err?.message ?? "Unknown failure");
}
}Example:
function riskyDivide(a: number, b: number) {
if (b === 0) throw new Error("Boom!");
return a / b;
}
const checked = handleSafely(riskyDivide, 10, 0);Converting ResultBox to a Throwing Function
If needed, convert the safe result back into a throwing workflow:
export function unwrapOrCrash<T>(supplier: () => ResultBox<T>): T {
const outcome = supplier();
if (outcome.status === "ok") return outcome.data;
throw new Error(outcome.message);
}Usage:
const finalAnswer = unwrapOrCrash(() => robustDivide(10, 2));Why never Often Fails in Real Projects
Using never does not automatically make code safer.
Common mistakes include:
- assuming
neverreplaces runtime validation - overusing it in public APIs
- hiding real errors behind “impossible” states
In production, never works best when combined with:
- discriminated unions
- explicit runtime checks
- strict compiler settings
Summary
never is not an error type.
It is a compiler tool used to ensure correctness and detect unreachable or invalid states.
Use never for:
- Exhaustiveness checks
- Detecting logic gaps
- Modeling impossible conditions
Do NOT use never for:
- Functions that throw
- Error-state modeling
- Representing branching outcomes
When used properly, never becomes one of the strongest static‑analysis tools in the TypeScript type system.