The `never` type and `error` handling in TypeScript
Tags: typescript, nextjs, beginners, programming
Published: 2024-06-14 16:21:53

One thing that I see more often recently is that folks find out about the never type, and start using it more often, especially trying to model error handling. But more often than not, they don’t use it properly or overlook some fundamental features of never. This can lead to faulty code that might act up in production, so I want to clear doubts and misconceptions, and show you what you can really do with never.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v3a9no1pxgxydduwwso3.png)


## _"never"_ and _"errors"_.
First of all, don’t blame developers for misunderstandings. The docs promote an example of never and error handling that is true if looked at in isolation, but it’s not the whole story. The example is this:

```TypeScript
// Function returning never must not have a reachable endpoint
function error(message: string): never {
  throw new Error(message);
}
```

This comes from the old docs, which are deprecated. The new docs do a much better job, yet this example sticks around in lots of places and is referenced in many blog posts out there.

It’s Schrödinger’s example: It’s both correct and wrong until you open the box and use it in situations that are not as simple as the one in the example.

Let’s look at the correct version. The example states that a function returning never must not have a reachable endpoint. Cool, so if I call this function, the binding I create will be unusable, right?

```TypeScript
function error(message: string): never {
  throw new Error(message);
}

const a = error("What is happening?");
//    ^? const a: never
```
Yes! The type of a is **_never_**, and I can’t do anything with it. What TypeScript checks for us is that this function won’t ever return a valid value. So it correctly approves that the **_never_**return type matches the error thrown.

But you rarely just break your code in a single function without some extra stuff going on. Usually, you have either a correct value or you throw something.

What I see people do is this:

```TypeScript
function divide(a: number, b: number): number | never {
  if (b == 0) {
    throw new Error("Division by Zero!");
  }
  return a / b;
}

const result = divide(2, 0);

if (typeof result === "number") {
  console.log("We have a value!");
} else {
  console.log("We have an error!");
}
```

You want to model your function in a way that in the _“good”_ case, you return a value of type **_number_**, and you want to indicate that this might return an error. So it’s **_number_** | **_never_**.

And this example is 100% bogus, wrong, and doesn’t express the truth at all! If you look at the type of result, you see that the type is only number. Where has never gone?

Again, I don’t blame the developers. If you look at the original example describing the never type, you might draw your conclusion that this is how you want to handle the error case.

But **I do blame bloggers for creating cheap Medium articles that reach the top hit on Google with wrong information that they didn’t even bother to test. I won’t link the culprit to not give them any link juice, but it’s easy to find with the right keywords. Kids, don’t do this. All your LLMs will learn the wrong things. And your readers, too.**

## What happened to "never"?
Alright, where did the **_never_** type go? It’s easy to understand if you know what never actually represents, and how it works in the type system.

The TypeScript type system represents types as sets of **_values_**. The type checker’s purpose is to make sure that a certain known value is part of a certain set of values. If you have a variable with the value 2, it will be part of the set of number. The type boolean allows for the values true and false. You can fine-grain your types and create unions making the set bigger, or intersections, making the set smaller.

The _**never**_ type also represents a set of values, the empty set. No value is compatible with never. It indicates a situation that should never happen. This is also known as a bottom type.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/125lbpfa7t2cnn4tfmff.png)


The reason why never disappeared is simple set theory. If you create a union of a set **_number_** and the empty set, well all that remains is **_number_**. If you add nothing to something, something remains, after all.

never gets swallowed up by reality, and you won’t be able to indicate that this function might return an error. The type system will just ignore it.

Take away the following: Don’t use never as a representation for a thrown Error.

## How to correctly use _never_ for error handling

This doesn’t render _**never**_ useless, though. There are situations where you can model impossible states with this type.

Think of expressing your models as a union type.

```TypeScript
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Rectangle = { kind: "rectangle"; width: number; height: number };

type Shape = Circle | Square | Rectangle;
```

Note that I set the kind property to a literal type. This is a discriminated union. Usually, when creating a union type, TypeScript will allow elements that fall into the overlapping areas of the sets, meaning that an object with **{ radius: 3, side: 4, width: 5 }** would be accepted as a Shape.

But by using a literal type, TypeScript can distinguish between the different types and will only allow the correct properties for each type. This is because ** "circle" | "square" | "rectangle"** don’t have any overlap.

Also, note that we use a literal string here as a type. This is not a value. **"circle"** is a type that only accepts a single value, the literal string called **"circle"**.

With this discriminated union, we can now use exhaustiveness checks to make sure that we handle all cases.

```TypeScript
function area(s: Shape): number {
  switch (s.kind) {
    case "circle":
      return Math.PI * s.radius ** 2;
    case "square":
      return s.side ** 2;
    case "rectangle":
      return s.width * s.height;
    default:
      // tbd
  }
}
```

You even get nice autocomplete in your editor and TypeScript will tell you which cases to handle.

We haven’t handled the _**default**_ case yet, but we can use never to indicate that this case should never happen.


```TypeScript
function assertNever(x: never): never {
  throw new Error("Unexpected object: " + x);
}

function area(s: Shape): number {
  switch (s.kind) {
    case "circle":
      return Math.PI * s.radius ** 2;
    case "square":
      return s.side ** 2;
    case "rectangle":
      return s.width * s.height;
    default:
      return assertNever(s);
  }
}
```

This is interesting. We have a **default** case that should never happen because our types won’t allow it, and we use **never** as a parameter type. Meaning that we pass a value, even though the **never** set doesn’t have any values. Something is going incredibly wrong if we reach this stage!

And we can use this to let TypeScript help us in the case that our code should change. Let’s introduce a new variant of Shape without changing the area function.

```TypeScript
type Triangle = { kind: "triangle"; a: number; b: number; c: number };

type Shape = Circle | Square | Rectangle | Triangle;

function area(s: Shape): number {
  switch (s.kind) {
    case "circle":
      return Math.PI * s.radius ** 2;
    case "square":
      return s.side ** 2;
    case "rectangle":
      return s.width * s.height;
    default:
      return assertNever(s);
    //                   ~
    // Argument of type 'Triangle' is not assignable
    // to parameter of type 'never'.
  }
}
```

Look at that! TypeScript understands that we didn’t check all variants, and our code will throw red squigglies at us. Time to check if we did everything right!

This is the good stuff about **never**. It helps you make sure that all your values are handled, and if not, it will tell you through red squigglies.


## Error types
You now know how **never** actually works, but you still want to have a way to correctly express errors.

There is a way that is inspired by functional programming languages and made popular by Rust. You can use a result type to express that a function might fail.
**We do the following:**

- We define a type **Error** that carries the error message and has a kind property set to **"error"**.
- We define a generic type **Success** that carries the value and has a kind property set to **"success"**.
- Both types are combined into a Result type, which is a union of **Error** and **Success**.
- We define two functions **error** and success to create the respective types.

**Like this:**

```TypeScript
type ErrorT = { kind: "error"; error: string };
type Success<T> = { kind: "success"; value: T };

type Result<T> = ErrorT | Success<T>;

function error(msg: string): ErrorT {
  return { kind: "error", error: msg };
}

function success<T>(value: T): Success<T> {
  return { kind: "success", value };
}
```

Let’s refactor the **divide** function from above to use this **Result** type.

```TypeScript
function divide(a: number, b: number): Result<number> {
  if (b === 0) {
    return error("Division by zero");
  }
  return success(a / b);
}
```

If we want to use the result, we need to check for the kind property and handle the respective case.

```TypeScript
const result = divide(10, 0);

if (result.kind === "error") {
  // result is of type Error
  console.error(result.error);
} else {
  // result is of type Success<number>
  console.log(result.value);
}
```
The important thing is that the types are correct, and the type **system** knows about all the possible states.

And you can play around with that. Maybe you have functions that throw errors. Create a **safe** function that takes the original function and its arguments, and wraps everything into your newly created error handling system.

```TypeScript
function safe<Args extends unknown[], R>(
  fn: (...args: Args) => R,
  ...args: Args
): Result<R> {
  try {
    return success(fn(...args));
  } catch (e: any) {
    return error("Error: " + e?.message ?? "unknown");
  }
}

function unsafeDivide(a: number, b: number): number {
  if (b == 0) {
    throw new Error("Division by Zero!");
  }
  return a / b;
}

const result = safe(unsafeDivide, 10, 0);
```

**Or** if you have a Result, and you want to fail at some point, well then do so:

```TypeScript
function fail<T>(fn: () => Result<T>): T {
  const result = fn();
  if (result.kind === "success") {
    return result.value;
  }
  throw new Error(result.error);
}

const a = fail(divide(10, 0));
```

It’s not perfect, but you have clear states, clear types, know about what your sets can contain, and when you really have no possible value left.

## Conclusion
I found some code piece expressing thrown **Errors** with **never** a while ago and thought “Oh, the docs messed something up”. I got into a rabbit hole seeing that folks on Medium are suggesting this as a good practice. If there’s something that annoys me, it’s folks teaching things wrong. So I wrote this article to clear things up. I hope it does. 

---

@Article by chinnanj