Command Palette

Search for a command to run...

Hectal
PHASE 2Beginner ~27 min· topic 3 of 7

Topic 2.3

void, never and Returning Nothing

In one line

void means a function gives back nothing you should use, and never means a function never gets back at all because it throws or loops forever. Both help the checker understand what happens after a call.

Think of it like this

A postbox and a trapdoor. You post a letter and walk away; nobody hands you anything back, and you don't expect them to. That is void: the job is done, there is nothing to use. A trapdoor is different: step on it and you never come back out the other side. That is never: the code after the call can't be reached.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

void
A return type meaning "no useful result". The caller must not use what comes back.
undefined
A real value meaning "nothing here". It can be returned and checked, unlike a void result.
never
The type of a value that can't exist. A function returns never when it always throws or never ends.
Callback
A function you pass to another function so that it can be called later.
Side effect
Something a function does besides returning a value, such as printing or saving.
Unreachable code
Code that can never run because everything before it throws or never ends.
Bottom type
A type with no values at all. never is the bottom type: it fits into every other type.
Throw
Stop the function right now and hand an error up to whoever called it.

Step by step

01void: it does something, it gives nothing

A function that prints has no result to use. Its return type is void, written by you or inferred when there is no return value.

The call has the type void. You can call it as a statement, but there is nothing to store.

void.tswhole filets
function logIt(message: string): void {
  console.log(message);
}
const r = logIt("hi");   // r has the type void

02A void callback may return something

The callback type () => void means "I will ignore what you return". So a function that returns a number fits.

That is why forEach accepts (n) => out.push(n) even though push returns the new length.

callback.tswhole filets
type Callback = () => void;
const cb: Callback = () => 42;     // fine
const v = cb();                    // v: void (the 42 is still there at runtime)

03never: this function does not come back

A function that always throws has the return type never. Write it on purpose to make a reusable "stop here" helper.

After a call to it, TypeScript knows the code below is unreachable on that path.

fail.tswhole filets
function fail(message: string): never {
  throw new Error(message);
}

04Using never to narrow

In must, if the value is undefined we call fail. The checker sees that this branch never continues, so below the if the value is a string.

This is the same idea as an early return or throw, but wrapped in a function with a good error message.

Using never to narrowdiagram
Rendering diagram…

05never in the type system

never fits into every type but nothing fits into never except never. In a union it vanishes (string | never is string).

A function that promises never must never reach its end. If it can, tsc says TS2534.

Try it yourself

  1. 1

    Make the callback strict

    In the second example change type Callback = () => void; to type Callback = () => undefined;. Predict: does () => 42 still fit? Read the new error. void is the lenient one.

  2. 2

    Remove the never

    In the third example change fail(message: string): never to fail(message: string): void. Predict what happens to must. The error moves to return value: TS2322: Type 'string | undefined' is not assignable to type 'string'. Without never TypeScript thinks the code can continue after fail, so value may still be undefined.

  3. 3

    Use a void result

    After the forgot function in the first example, add const total: number = logIt("x");. Read the message and explain why using a void result as a value is blocked.

Code & diagrams

void, undefined and the return statement

logIt's result has the type void. giveUndefined really returns the value undefined. forgot() has no return at all and is accepted (TypeScript 5.1). wrongVoid is the only error: a declared void can't return a number.

Console

(nothing was printed)

Expected console output

hi
undefined undefined
A void callback type accepts functions that return values

The 42 is really returned (the console shows it), but TypeScript gives v the type void, so you can't use it as a number. map is different: it keeps what the callback returns, so the array is void[].

Console

(nothing was printed)

Expected console output

42
[ 2, 4, 6 ]
never: functions that throw, and what TypeScript infers

must returns a string on every path because fail never comes back; change fail's return type to void and read the new error. The arrow function that throws is inferred as never, but the function declaration is inferred as void. broken promised never and reaches its end.

Console

(nothing was printed)

Expected console output

ready
Error: a value is required
never is the type nothing fits into

A never value fits anywhere (n is a number), but a 5 doesn't fit into never. In the union string | never the never part disappears.

Console

(nothing was printed)

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Return a value from a void function

Look at wrongVoid in the first example: a function declared : void that does return 42;. (Change : void to : number and the error disappears.)

terminal
$ Types panel
── what you'll see ──
index.ts(13,3): error TS2322: Type 'number' is not assignable to type 'void'.

Break #2

A never function that comes back

Look at broken in the third example: function broken(): never { console.log("returns"); } (its body ends without throwing).

terminal
$ Types panel
── what you'll see ──
index.ts(19,20): error TS2534: A function returning 'never' cannot have a reachable end point.

Myth vs fact

Myth

void and undefined mean the same thing.

Fact

undefined is a value you can return and check. void means "ignore the result". undefined fits into void, but a void result is not assignable to undefined.

Myth

A callback declared () => void must not return anything.

Fact

It means the caller ignores the result, so any function fits. That is why forEach(n => out.push(n)) compiles.

Myth

A function that only throws is automatically never.

Fact

Only arrow functions and function expressions are inferred as never. A function declaration that throws is inferred as void. Write : never to be explicit, and to make the call affect control flow.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    For a call to a never function to narrow control flow, TypeScript needs to know its type without looking at the call: the function must be a declaration, or a const with an explicit type annotation. A const fail = (m: string) => { throw ... } with no annotation gets the inferred type (m: string) => never, but calls to it do not narrow the code below, because the checker only trusts declared types here.

  • ▸

    The leniency of () => void is a special case in assignability: a function type with a void return accepts any return type. It is also why a () => void callback accepts an async function (which returns a Promise), and a rich source of subtle bugs when a Promise is returned from a void callback and nobody awaits it.

  • ▸

    never is the empty union. That is why Exclude<T, U> and distributive conditional types remove cases by turning them into never (Topic 10.2), and why an exhaustive switch can leave a variable of type never (Topic 4.6).

Remember this

  1. 1

    The problem this solves: Some functions give back nothing useful, and some never return at all. If the types can't say this, someone uses a result that isn't there, or a code path is quietly forgotten. Many functions do something (print a line, save a file, change a page) and have no result to give. In JavaScript they return undefined by accident. TypeScript has a type to say "don't use the result": **void**. A function that prints and ends has the return type void, and you can write it yourself: function logIt(message: string): void.

  2. 2

    void and undefined are close but not the same. undefined is a real value that a function may return and you may use. void says "whatever comes back, ignore it". You can assign undefined to a void variable, but not the other way round: const a: undefined = logIt("x") is TS2322: Type 'void' is not assignable to type 'undefined'. Use void for "returns nothing"; use undefined only when the function really returns that value and the caller may look at it.

  3. 3

    A function declared : void may not return 42: that is TS2322: Type 'number' is not assignable to type 'void'. But there is a deliberate exception for function types: a function whose type says it returns void may be given a function that returns something. const cb: () => void = () => 42 is fine, and so is names.forEach((n) => out.push(n)) even though push returns a number. The caller promises to ignore the result, so it is safe, and without this rule you could not pass most functions where a callback is expected.

  4. 4

    The exception has a catch. The result is still a real value at runtime; TypeScript just stops you from using it, because its type is void. const v = cb() has the type void, and at runtime v is 42. Don't rely on that: treat void results as unusable. Also, only function types and contexts like callbacks get this leniency. A function declaration with an explicit : void return type is still strict.

  5. 5

    **never** is the type of something that can't exist: a value that never happens. A function returns never when it can't finish normally: it always throws an error or loops forever. function fail(message: string): never { throw new Error(message); }. Writing never makes TypeScript check you: a function that says never but can reach its end is TS2534: A function returning 'never' cannot have a reachable end point.

  6. 6

    never helps the checker understand the rest of your code. After a call to a function that returns never, TypeScript treats the code below as unreachable, so after if (value === undefined) { fail("missing"); } the value is known to be a string. This only works when the function's never is written explicitly (a declared function or a variable with an annotated type); a function declaration that just throws, without the annotation, has the inferred type void, while an arrow function or function expression that always throws is inferred as never.

  7. 7

    never is also the bottom type: it is assignable to every other type (there is no value to be wrong about) and nothing but never can be assigned to it (let v: never; v = 5 is an error). In a union it disappears, since it adds no possible values: string | never is just string. You'll use this in Topic 4.6 (exhaustiveness checks) and in conditional types much later.

Explain it without notes

01

What is the difference between void and undefined as a return type?

02

Why can forEach accept a callback that returns a number?

03

What does the never return type mean, and when do you use it?

04

What is never in the type system, and why does string | never equal string?

Practice

01

Write function notify(message: string): void that logs the message. Try to log its result into a string variable and note the error. Then write a callback of type () => void that returns a string and show that it type-checks.

02

Write function assertNever(value: never): never that throws an error with the text Unexpected: and the value. Use it to finish a function size(shape: "small" | "large"): number with an if per case.

03

Write function mustGet(prices: Map<string, number>, item: string): number that returns the price, or calls a never helper fail when the item is missing. Explain why it type-checks although Map.get can return undefined.

Trade-offs

  • ↔

    The lenient void callback rule makes callbacks easy to write, but it also hides cases where you returned a value that you expected to be used (like a Promise nobody awaits). Linters (no-misused-promises) catch that.

  • ↔

    A never-returning helper centralises error messages and keeps happy-path code flat, but it is invisible in the signature of the callers, so readers must know fail always throws. Name it so that it is obvious (fail, assertNever, unreachable).

Done when you can

  • I can explain void versus undefined in one sentence.

  • I can explain why a callback typed () => void accepts a function that returns a number.

  • I can write a never function and explain TS2534.

  • I can say what is inferred for an arrow function and a function declaration that only throw.

  • I can explain why string | never is string.