Practical Guide to Fp-ts: P3 — Task, Either, TaskEither
Tags: functional, fpts, typescript
Published: 2020-08-20 13:42:13

---
layout: 'post'
title: "Practical Guide to Fp-ts: P3 — Task, Either, TaskEither"
description: "Learn how to use Fp-ts practically. Introduces the task, either, and taskEither operators. No mathematical knowledge required."
tags: fp, fpts, typescript
canonical_url: https://rlee.dev/practical-guide-to-fp-ts-part-3
series: Practical Guide to Fp‑ts
published: true
---

## Introduction

This is the third post in my series on learning fp-ts the practical way. In my last post, I introduced the [Option](https://gcanti.github.io/fp-ts/modules/Option.ts.html) type and the `map`, `flatten`, and `chain` operators.

This post will introduce two concepts in fp-ts: asynchronous tasks and error handling. Namely we will look at the [Task](https://gcanti.github.io/fp-ts/modules/Task.ts.html), [Either](https://gcanti.github.io/fp-ts/modules/Either.ts.html), and [TaskEither](https://gcanti.github.io/fp-ts/modules/TaskEither.ts.html) types.

## Task

![Task-Promise](https://rlee.dev/assets/blog/practical-guide-to-fp-ts/successful-promise.png)

Every asynchronous operation in modern Typescript is done using a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) object. A task is a function that returns a promise which is **expected** to **never** be rejected.

The [type definition](https://gcanti.github.io/fp-ts/modules/Task.ts.html) for task can be found below.

```ts
interface Task<A> {
  (): Promise<A>
}
```

Another way to define task is using a function type definition.

```ts
type Task<A> = () => Promise<A>
```

Tasks are expected to always succeed but can fail when an error occurs outside our expectations. In this case, the error is thrown and breaks the functional pipeline. An analogy to this is `awaiting` a Promise that throws an error without putting a `try-catch-finally` block in front. Test your assumptions before using Task.

### Why use Tasks?

A Task is more than a glorified promise; it is also an **expression of intent**.

From a client perspective, when you are using a library, all asynchronous functions will have a type definition that returns a `Promise<T>`. Some of the functions might never fail but are asynchronous out of necessity. A Promise provides no indication about whether the function can fail. As such, in the imperative model, you are forced to handle these errors using a `try-catch-finally` block.

By using `Task<T>`, we relieve the burden on the client to handle errors that don't exist.

### When can an operation "never fail"?

In the age of distributed computing, errors are the norm. Languages like Go and Rust embrace this model by [forcing you to handle errors](https://doc.rust-lang.org/book/ch09-00-error-handling.html). To understand when an operation can never fail, we must first understand the most common ways a function can fail in the first place.

Functions commonly fail because of invalid preconditions. Take the function below, where the precondition is the length of `id` must be less than 36.

```ts
async function someTask(id: string) {
  if (id.length > 36) {
    throw new Error('id must have length greater than 36')
  }

  // do async work here
}
```

If we knew the exact implementation of the function and we knew all errors stem from pre-condition failing, then we can assume the function will never fail if and only if we know the length of `id` is `<=` 36. As such, we can wrap the function into a Task and argue it never fails.

```ts
const id = 'abc'
const task: T.Task<void> = () => someTask(id)
```

In general, we don't make these assumptions because we don't always know the implementation. It's also _risky_ because the implementation can change without us knowing.

### Handled Failures Can't Fail

A more real-world example is when you have an operation that can fail, but is handled by reducing both the success and failure outcomes into a single type. Since the error has been handled, the function, although asynchronous, will always return a `Promise` that is fulfilled.

Take this function that reduces both the success and failure outcomes into a boolean result.

```ts
async function boolTask(): Promise<boolean> {
  try {
    await asyncFunction()
    return true
  } catch (err) {
    return false
  }
}
```

By definition, this function already implements the `Task` interface, but because the return type is a `Promise`, the result is still ambiguous to the client. We can remove the ambiguity by adjusting the syntax.

```ts
import { Task } from 'fp-ts/lib/Task'

const boolTask: Task<boolean> = async () => {
  try {
    await asyncFunction()
    return true
  } catch (err) {
    return false
  }
}
```

### Constructors

Any arbitrary value can become a Task by using the `of` operator to lift it into the `Task` world. This is equivalent to calling `Promise.resolve`.

```ts
import * as T from 'fp-ts/lib/Task'

const foo = 'asdf' // string
const bar = T.of(foo) // T.Task<string>

// Same As
const fdsa: T.Task<string> = () => Promise.resolve(foo)
```

## Either

An Either is a type that represents a _synchronous_ operation that can succeed or fail. Much like Option, where it is `Some` or `None`, the Either type is either `Right` or `Left`. `Right` represents success and `Left` represents failure. It is analogous to the [Result](https://doc.rust-lang.org/std/result/) type in Rust.

As such, we get the following type definitions.

```ts
type Either<E, A> = Left<E> | Right<A>

export interface Left<E> {
  readonly _tag: 'Left'
  readonly left: E
}

export interface Right<A> {
  readonly _tag: 'Right'
  readonly right: A
}
```

The Either type is a union type of `Left` and `Right`. The `_tag` field is used as a [discriminator](https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions) to differentiate between `Left` and `Right`.

### Why use Eithers

Eithers are essential for capturing error states in functional programming. We need the Eithers because we cannot break pipelines by throwing errors. Error states must either be handled or propagated up the call stack.

Eithers are also advantageous to their `try-catch-finally` counterparts because the error is always type-safe. When you use a `catch` block, the [error is always of type `unknown`](https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-beta/#unknown-on-catch). This is inconvenient for you as the client because you need to use `instanceof` to narrow down the error type. Even worse is when you are forced to define your own [custom type guards](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types) to do the same thing.

With Eithers, we know every possible error state based on the type signature. We can choose to handle them in a switch statement or continue to propagate up the call stack.

### Eithers in Action

Let’s contrive an example where we are validating a password for security. The password must be at least 8 characters long and have at least 1 capital letter. If the password is valid, we will hash it using a very insecure `md5` hash.

1. Create 2 error classes to represent the two different error states. Join them together into a discriminated union.

```ts
// password.ts

export class MinLengthValidationError extends Error {
  public _tag: 'PasswordMinLengthValidationError'

  public minLength: number

  private constructor(minLength: number) {
    super('password fails to meet min length requirement: ${minLength}')
    this._tag = 'PasswordMinLengthValidationError'
    this.minLength = minLength
  }

  public static of(minLength: number): MinLengthValidationError {
    return new MinLengthValidationError(minLength)
  }
}

export class CapitalLetterMissingValidationError extends Error {
  public _tag: 'PasswordCapitalLetterMissingValidationError'

  private constructor() {
    super(`password is missing a capital letter`)
    this._tag = 'PasswordCapitalLetterMissingValidationError'
  }

  public static of(): CapitalLetterMissingValidationError {
    return new CapitalLetterMissingValidationError()
  }
}

export type PasswordValidationError =
  | MinLengthValidationError
  | CapitalLetterMissingValidationError
```

Note we are using the `Error` class instead of declaring the error as a plain object because it comes with built-in stack-trace, which is necessary for debugging.

2. Declare the Password Type

```ts
// password.ts

export interface Password {
  _tag: 'Password'
  value: string
  isHashed: boolean
}
```

3. Create the constructors for the Password Type

```ts
// password.ts

export function of(value: string): Password {
  return { _tag: 'Password', value, isHashed: false }
}

export function fromHashed(value: string): Password {
  return { _tag: 'Password', value, isHashed: true }
}
```

4. Validate the password using a Password specification.

```ts
// password.ts

export type PasswordSpecification = {
  minLength?: number
  capitalLetterRequired?: boolean
}

export function validate({
  minLength = 0,
  capitalLetterRequired = false,
}: PasswordSpecification = {}) {
  return (password: Password): E.Either<PasswordValidationError, Password> => {
    if (password.value.length < minLength) {
      return E.left(MinLengthValidationError.of(minLength))
    }

    if (capitalLetterRequired && !/[A-Z]/.test(password.value)) {
      return E.left(CapitalLetterMissingValidationError.of())
    }

    return E.right({ ...password, isValidated: true })
  }
}
```

Notice how `validate` doesn't return a Password type directly, but a function that returns a Password type. We could have put the `PasswordSpecification` and Password as parameters to a single function, but the reason why we want to separate them is to make function chaining easier.

When we construct the Password using `of` or `fromHashed`, we want to directly pipe the result of that function, `Password`, into the next function. If our `validate` function were to take two parameters instead of one, it would break the whole flow. This methodology of splitting function parameters is called [currying](https://javascript.info/currying-partials).

You may also notice we can only propagate a single error upwards. But what if multiple validations fail? It would be better to propagate all of them. We will learn about this in the next post.

5. Define a hash function that takes a curried hash function.

```ts
// password.ts

export type HashFn = (value: string) => string

export function hash(hashFn: HashFn) {
  return (password: Password): Password => ({
    ...password,
    value: hashFn(password.value),
    isHashed: true,
  })
}
```

6. Create a pipeline

```ts
// index.ts

import { flow, identity, pipe } from 'fp-ts/lib/function'
import * as Password from './password'
import crypto from 'crypto'
import * as E from 'fp-ts/lib/Either'

const pipeline = flow(
  Password.of,
  Password.validate({ minLength: 8, capitalLetterRequired: true }),
  E.map(
    Password.hash((value) =>
      crypto.createHash('md5').update(value).digest('hex'),
    ),
  ),
)
```

7. Test using an invalid password

```ts
console.log(pipe('pw123', pipeline))
```

Produces the following:

```ts
{
  _tag: 'Left',
  left: Error: password fails to meet min length requirement: 8
      at new MinLengthValidationError (/tmp/either-demo/password.ts:9:5)
      at Function.MinLengthValidationError.of (/tmp/either-demo/password.ts:15:12)
      at /tmp/either-demo/password.ts:61:46
      at /tmp/either-demo/node_modules/fp-ts/lib/function.js:92:27
      at Object.pipe (/tmp/either-demo/node_modules/fp-ts/lib/function.js:190:20)
      at Object.<anonymous> (/tmp/either-demo/index.ts:16:13)
      at Module._compile (internal/modules/cjs/loader.js:1118:30)
      at Module.m._compile (/tmp/either-demo/node_modules/ts-node/src/index.ts:858:23)
      at Module._extensions..js (internal/modules/cjs/loader.js:1138:10)
      at Object.require.extensions.<computed> [as .ts] (/tmp/either-demo/node_modules/ts-node/src/index.ts:861:12) {
    _tag: 'PasswordMinLengthValidationError',
    minLength: 8
  }
}
```

Due to the way Node prints errors, `left` doesn't look like a regular
typescript object. The underlying object looks like this.

```ts
{
  _tag: 'Left',
  left: {
    message: 'password fails to meet min length requirement: 8',
    stack: `Error: password fails to meet min length requirement: 8
      at new MinLengthValidationError (/tmp/either-demo/password.ts:9:5)
      at Function.MinLengthValidationError.of (/tmp/either-demo/password.ts:15:12)
      at /tmp/either-demo/password.ts:61:46
      at /tmp/either-demo/node_modules/fp-ts/lib/function.js:92:27
      at Object.pipe (/tmp/either-demo/node_modules/fp-ts/lib/function.js:190:20)
      at Object.<anonymous> (/tmp/either-demo/index.ts:16:13)
      at Module._compile (internal/modules/cjs/loader.js:1118:30)
      at Module.m._compile (/tmp/either-demo/node_modules/ts-node/src/index.ts:858:23)
      at Module._extensions..js (internal/modules/cjs/loader.js:1138:10)
      at Object.require.extensions.<computed> [as .ts] (/tmp/either-demo/node_modules/ts-node/src/index.ts:861:12)`
    _tag: 'PasswordMinLengthValidationError',
    minLength: 8
  }
}
```

8. Test using a valid password.

```ts
console.log(pipe('Password123', pipeline))
```

Produces the following:

```ts
{
  _tag: 'Right',
  right: {
    _tag: 'Password',
    value: '42f749ade7f9e195bf475f37a44cafcb',
    isHashed: true,
    isValidated: true
  }
}
```

### Chaining Eithers

What if `hash` was an operation that could also fail and return an `Either`? We can `chainW` operator to chain both `validate` and `hash` into a single Either type. We'll use the base `Error` type to represent this error for simplicity's sake.

1. Update the hash function to return an Either

```ts
export type HashFn = (value: string) => E.Either<Error, string>

export function hash(hashFn: HashFn) {
  return (password: Password): E.Either<Error, Password> =>
    pipe(
      hashFn(password.value),
      E.map((value) => ({
        ...password,
        value,
        isHashed: true,
      })),
    )
}
```

2. Update the pipeline using `chainW`

```ts
const pipeline = flow(
  Password.of,
  Password.validate({ minLength: 8, capitalLetterRequired: true }),
  E.chainW(
    Password.hash((value) =>
      E.right(crypto.createHash('md5').update(value).digest('hex')),
    ),
  ),
)
```

The reason why we use `chainW` instead of `chain` because we want to widen the final type to include both errors from `validate` and `hash`. If you hover over `pipeline` to inspect the type, this is what you would get.

```ts
E.Either<
  MinLengthValidationError | CapitalLetterMissingValidationError | Error,
  Password
>
```

But if we swap `chainW` with `chain`, we would only get the final error type in the chain.

```ts
E.Either<Error, Password.Password>
```

But note, `chain` only works here because `Error` is a superclass of all 3 of our errors. If the left side of the generic to the function `hash` was not an Error, we would be forced to use `chainW` to cover the two Errors from `validate`.

You can run the source code [here](https://repl.it/@ryanleecode/EitherExample).

## TaskEither

We know a Task is an asynchronous operation that can't fail. We also know an Either is a synchronous operation that can fail. Putting the two together, a TaskEither is an **asynchronous** operation that **can fail**.

Performing an HTTP request is a good demonstration of this functionality.

```ts
import axios from 'axios'
import { pipe } from 'fp-ts/lib/function'
import * as TE from 'fp-ts/lib/TaskEither'
;(async () => {
  const ok = await pipe(
    TE.tryCatch(
      () => axios.get('https://httpstat.us/200'),
      (reason) => new Error(`${reason}`),
    ),
    TE.map((resp) => resp.data),
  )()

  console.log(ok)
  // { _tag: 'Right', right: { code: 200, description: 'OK' } }
})()
```

Here we are making an http request using axios to `httpstat` which returns status code `200`. An error will not occur because the http response is 200 –⁠ Ok. The right side gets printed out.

We can do the same thing for a `500` status code.

```ts
type Resp = { code: number; description: string }
;(async () => {
  const result = await pipe(
    TE.tryCatch(
      () => axios.get('https://httpstat.us/500'),
      (reason) => new Error(`${reason}`),
    ),
    TE.map((resp) => resp.data),
  )()

  console.log(result)
  /**
   * {
   *   _tag: 'Left',
   *   left: Error: Error: Request failed with status code 500
   *       at /tmp/either-demo/taskeither.ts:19:19
   *       at /tmp/either-demo/node_modules/fp-ts/lib/TaskEither.js:94:85
   *       at processTicksAndRejections (internal/process/task_queues.js:97:5)
   * }
   */
})()
```

## Folding

If we're hitting the `https://httpstat.us/200` endpoint, we can assume the operation will succeed and use the `fold` operator to convert the output into a `Task`.

```ts
import { absurd, constVoid, pipe, unsafeCoerce } from 'fp-ts/lib/function'

const result = pipe(
  TE.tryCatch(
    () => axios.get('https://httpstat.us/200'),
    () => constVoid() as never,
  ),
  TE.map((resp) => unsafeCoerce<unknown, Resp>(resp.data)),
  TE.fold(absurd, T.of),
) // Not executing the promise

// Result is of type:
// T.Task<Resp>
```

Notice how I'm passing `T.of` directly instead of creating an anonymous function that calls `T.of`. i.e. `(a) => T.of(a)`.

Absurd is a function that takes a `never` and casts it to a generic type `A`, which in this case is `Resp`.

## Asynchronously Error Handling

Sometimes your error handling is also asynchronous and this is common if you're doing a [2 Phase Commit](https://en.wikipedia.org/wiki/Two-phase_commit_protocol). A good example is when you are processing a database transaction.

```ts
import { pipe } from 'fp-ts/lib/function'
import * as TE from 'fp-ts/lib/TaskEither'

declare function begin(): Promise<void>
declare function commit(): Promise<void>
declare function rollback(): Promise<void>

const result = pipe(
  TE.tryCatch(
    () => begin(),
    (err) => new Error(`begin txn failed: ${err}`),
  ),
  TE.chain(() =>
    TE.tryCatch(
      () => commit(),
      (err) => new Error(`commit txn failed: ${err}`),
    ),
  ),
  TE.orElse((originalError) =>
    pipe(
      TE.tryCatch(
        () => rollback(),
        (err) => new Error(`rollback txn failed: ${err}`),
      ),
      TE.fold(TE.left, () => TE.left(originalError)),
    ),
  ),
)
```

In this example, we try to rollback if the begin or commit operations fail and return the original error. If rollback also fails, we return the rollback error.

## Conclusion

Error handling and asynchronous operations are core components of any application. By understanding Task, Either, and TaskEither, you now have the building blocks you need to develop a simple application.

If you found this post helpful, be sure to also follow me on [Twitter](https://twitter.com/ryanleecode).