Void and Never in TypeScript
Void means a function returns nothing useful. Never means a function never returns at all. Learn the difference, when to use each, and how never enables exhaustiveness checking.
Void and never in TypeScript both describe the absence of a value, but they mean very different things. void means a function finishes but you should not care about what it returns. never means a function never finishes at all, or that a particular code path is impossible to reach.
The table below captures the difference at a glance:
| Behavior | void | never |
|---|---|---|
| Function returns? | Yes, returns undefined | No, never finishes |
| Caller can use the return value? | Discouraged but possible | Not possible |
| Common use | Side-effect functions, callbacks | Functions that throw, exhaustiveness checks |
| Represents | "I do not care about the return" | "This cannot happen" |
Void: A Function That Returns Nothing Useful
A function with a void return type finishes normally but does not produce a meaningful return value. It runs for its side effects, such as logging, updating the DOM, or writing to a database.
function logMessage(message: string): void {
console.log(message);
// No return statement
}Calling this function runs it and produces undefined at runtime, but the void type tells callers not to rely on that value:
const result = logMessage("Hello");
// result is void, using it would be a mistakevoid is also the type TypeScript infers automatically when a function has no return statement or only an empty return, so you rarely need to write it by hand for simple side-effect functions.
Void in Callbacks
void is common in callback types where you do not care what the callback returns:
type ClickHandler = (event: MouseEvent) => void;
const handleClick: ClickHandler = (event) => {
console.log(event.clientX, event.clientY);
};A void callback type has a special rule: the implementation is still allowed to return a value, and TypeScript will not complain, because the caller has already promised not to use it.
const handleClick: ClickHandler = (event) => {
return event.clientX; // ok: void accepts any return value and discards it
};This special rule applies only to void in callback positions. It exists so that array methods like forEach, which type their callback as void, can still accept arrow functions with an implicit expression body without a type error.
Void vs Undefined
A function annotated with : undefined behaves differently from one annotated : void. With undefined, the function must explicitly return that value:
function logVoid(message: string): void {
console.log(message); // ok: no return needed
}The undefined version requires an explicit return statement even when there is nothing meaningful to send back, which is the opposite of how the void version above behaves without one:
function logUndefined(message: string): undefined {
console.log(message);
return; // required: must explicitly return
}Use void when the caller should ignore the return value entirely. Use undefined only when the return value matters and you want to be explicit that it is always undefined.
Never: A Function That Never Returns
A function with a never return type never finishes executing. It either throws an error or runs forever, so the caller never actually gets a return value.
function throwError(message: string): never {
throw new Error(message);
}An infinite loop produces the same guarantee through a different route: control never reaches the end of the function, so the return type is still never, exactly as it was for the function that throws above.
function infiniteLoop(): never {
while (true) {
console.log("running...");
}
}Because TypeScript knows a never-returning function cannot finish normally, it treats any code that would run right after the call as unreachable, and uses that fact to narrow types earlier in the function:
function fail(message: string): never {
throw new Error(message);
}
function process(value: string | null) {
if (value === null) {
fail("value is required");
}
console.log(value.toUpperCase()); // value is string here
}TypeScript understands that fail never returns, so after the if block, value must be string. This is control flow analysis combined with the never type, and it works even though fail never uses a return statement at all.
Using Never for Exhaustiveness Checking
The most practical use of never is guaranteeing that a switch statement handles every possible case. When you work with discriminated unions, never can prove that you have not missed a variant.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };A function that computes the area checks the kind discriminant in a switch, with one case per shape and a default branch left over for anything unhandled:
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
default:
const exhaustive: never = shape;
return exhaustive;
}
}Every branch of the switch narrows shape to one specific variant, so by the time control reaches default, nothing is left; the type of shape there is never, and the assignment compiles cleanly.
If you later add a new shape variant but forget to handle it in the switch, the default branch no longer narrows down to nothing:
Type '{ kind: "triangle"; base: number; height: number; }' is not assignable to type 'never'.This is called exhaustiveness checking. It turns a missing case from a silent bug into a compiler error. For more on discriminated unions, see discriminated unions in TypeScript.
Void vs Never in Practice
The difference is easiest to see side by side. log finishes normally, so execution continues after the call:
function log(message: string): void {
console.log(message);
}The terminate function never finishes, so TypeScript treats any line written directly after a call to it as unreachable, the same way it treated the code after the fail function in the earlier example:
function terminate(message: string): never {
console.error(message);
throw new Error(message);
}Calling terminate inside a guard clause has the same narrowing effect as the fail example earlier: TypeScript knows execution cannot continue past it, so the rest of the function treats the value as already validated.
function example(value: string | null) {
if (value === null) {
terminate("value was null");
}
console.log(value.length); // value is string here
}For log, the caller keeps going. For terminate, the caller's code path ends. This is the fundamental difference: void finishes, never does not.
The flowchart below shows how TypeScript reasons about each type after a call:
The diagram illustrates why never is special: after a never-returning call, TypeScript treats subsequent code as dead and uses that information to narrow types in the caller.
Common Mistakes
Confusing Void With Never
// Wrong: a function that logs and returns nothing is void, not never
function log(message: string): never {
console.log(message);
}A function that finishes normally cannot have a never return type, so this fails to compile with an error explaining that a never-returning function cannot have a reachable end point. Use void for functions that complete without a meaningful return value.
Forgetting That Void Returns Undefined at Runtime
function log(message: string): void {
console.log(message);
}
const result = log("hello");
console.log(result); // undefined (at runtime)At compile time, result is void and TypeScript discourages using it. At runtime, it is plain undefined. The void type is a signal to humans and the compiler, not a runtime value.
Not Using Never for Exhaustiveness Checks
type Status = "loading" | "success" | "error";A handler for this kind of status type often ends with a default branch that has no never check at all, which looks harmless until the set of possible statuses changes:
function handle(status: Status) {
switch (status) {
case "loading":
return "Loading...";
case "success":
return "Done!";
default:
return "Something went wrong";
}
}Without that check, if someone later adds "idle" to Status, it silently falls through to the generic error message instead of failing to compile. Add a default never assertion whenever you intend to cover every case:
default:
const _exhaustive: never = status;
return _exhaustive;This way, adding a new status value becomes a compiler error instead of a silent bug. For more patterns like this, see exhaustive checks in TypeScript.
Rune AI
Key Insights
- void is for functions that return nothing the caller should use.
- never is for functions that never return: they throw, loop forever, or the code path is impossible.
- A void function implicitly returns undefined, but callers are discouraged from using it.
- Use never in a default branch to guarantee a switch handles every case.
- TypeScript narrows to never when all possible types have been eliminated.
Frequently Asked Questions
What is the difference between void and undefined as a return type?
Can a function have a never return type?
Why does TypeScript use never for unreachable code?
Conclusion
Void and never serve opposite purposes. Void says a function finishes but you should ignore its return value. Never says a function does not finish at all, or that a code path is impossible. Use void on event handlers, callbacks, and functions with side effects. Use never for functions that throw, for exhaustiveness checking in switch statements, and for places in your code that should be unreachable.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.