Schrodinger's function
Tags: javascript, typescript, functional
Published: 2022-09-27 19:07:04

> Promises and `throw` considered harmful

Take for example the code below

```ts
function createUser(email: string): User {
    if (users.includes(email)) {
        throw new Error(`User with email ${email} already exists`)
    }

    ...

    return newUser;
}
```

or the async version

```ts
function createUser(email: string): Promise<User> {
    return new Promise((resolve, reject) => {
        if (users.includes(email)) {
            throw new Error(`User with email ${email} already exists`)
        }

        ...

        return newUser;
    })
}
```

```ts
try {
    const result = createUser('email');
    ...
} catch (error /* error is not typed */) {
    ...
}
```

There's no way for whomever calls any of those functions to know that the function call might result in an error and what type of error to expect - without calling the function and seeing the result at runtime. Or inspecting the source code, most especially when dealing with third party libraries.

Which is why I call functions like that Schrodinger's function, you'll never know if they error or not until you call them or inspect them.

> I try to avoid directly returning promises for async operations (most especially async operations that may fail), because promises can reject without any indication what the error will be and are hard to `type` the error value.

So a principle I go by is to never reject a promise or throw an error. One might say "but those were added to JavaScript for a good reason", yes, but they also make your code unpredictable and hard to type check. For Typescript users, you'd have to resort to type casting and if statements to assert the error type.

## Solution

The approach I take is to return a `Result`, that can be one of two types, an `Err` or an `Ok` result. Essentially, the promise resolves with either an `Err` or an `Ok` value. E.g

```ts
type Err<T> = { _tag: "Err"; value: T };
type Ok<T> = { _tag: "Ok"; value: T };

type Result<E, A> = Err<E> | Ok<A>;
```

We've declared our `Result` type as a union of `Err` and `Ok`. So for example, we can write

```ts
try {
    let file = await readFile(...);
    return {_tag: "Ok", value: file}
} catch (err) {
    return {_tag: "Err", value: err}
}
```

We now have a fully typed return value for our async operation, calling this function gives us

```ts
let result = ... // {_tag: "Err", value: ...} | {_tag: "Ok", value: ...}

if (result._tag === "Err") {
    // value is of whatever type we defined as error
}
```

So now let's rewrite those functions we saw earlier

```ts
function createUser(email: string): Result<Error, User> {
    if (users.includes(email)) {
        return {_tag: "Err", value: new Error(`User with email ${email} already exists`)}
    }

    ...

    return {_tag: "Ok", value: newUser};
}

...
```

And some utility functions to help us distinguish between the result types

```ts
function isErr<E, A>(result: Result<E, A>): result is Err<E> {
  return result._tag === "Err";
}

function isOk<E, A>(result: Result<E, A>): result is Ok<A> {
  return result._tag === "Ok";
}
```

And finally, calling our functions

```ts
const result = createUser("email");

if (isErr(result)) {
  console.log(result.value); // Error()
}

// our result is `Ok` here
console.log(result.value); // User
```

This is just a quick run through of what is called an `Either` or `Maybe` in functional programming. If you're interested in diving deep into other ideas like this, checkout [fp-ts](https://gcanti.github.io/fp-ts/)