Combining Promises and Error Handling in TypeScript Using neverthrow

Combining Promises and Error Handling in TypeScript Using neverthrow

Handling asynchronous operations in TypeScript can benefit from a more type-safe and readable approach. In this article, we will explore a piece of code that demonstrates how to combine multiple asynchronous operations and handle their results in a type-safe manner.

import { Result, Err, Ok } from 'neverthrow'

type CombinePromisesResult<D extends Record<string, unknown>, E> = {
  [P in keyof D]: Result<UnwrapPromise<D[P]>, E>
}

type UnwrapPromise<P extends unknown> = P extends PromiseLike<infer V> ? V : P

export const combinePromises = async <
  D extends Record<string, unknown>,
  E = Error,
>(obj: D): Promise<CombinePromisesResult<D, E>> => {
  const keys = Object.keys(obj)
  const promises = keys.map(
    (key) =>
      obj[key as keyof D] as Promise<Result<UnwrapPromise<D[keyof D]>, E>>
  )
  const results = await Promise.all(promises)
  const data: Record<string, Result<UnwrapPromise<D[keyof D]>, E>> = {}
  results.forEach((result, i) => {
    data[keys[i]] = result
  })
  return data as CombinePromisesResult<D, E>
}

export type User = {
  id: string
  name: string
}

export const fetchUser = (): Promise<Result<User, Error>> => {
  const data: User = {
    id: "1",
    name: "jet"
  }
  // return Promise.resolve(new Ok(data))
  return Promise.resolve(new Err(new Error('Failed to fetch user.')))
}

export type Comment = {
  id: string
  content: string
  reviewedBy: string
}

export const fetchComments = (): Promise<Result<Comment[], Error>> => {
  const data: Comment[] = [{
    id: "1",
    content: "a",
    reviewedBy: "fei"
  }, {
    id: "2",
    content: "b",
    reviewedBy: "spike"
  }]
  return Promise.resolve(new Ok(data))
  return Promise.resolve(new Err(new Error('Failed to fetch comments.')));
}

export type Company = {
  id: string
  name: string
}

export const fetchCompany = (): Promise<Result<Company, Error>> => {
  const data: Company = {
    id: "1",
    name: "cowboy bebop"
  }
  return Promise.resolve(new Ok(data))
  return Promise.resolve(new Err(new Error('Failed to fetch company.')))
}

Introduction to the neverthrow Library

At the beginning of the code, we import three types/classes from neverthrow: Result, Err, and Ok. The neverthrow library assists in functional error handling, using the Result type to represent two states: success and failure.

Type Definitions

CombinePromisesResult

This type is responsible for associating the result of asynchronous operations with each property of an object. The result is of type Result, holding either successful data or an error.

UnwrapPromise

A utility type designed to extract the type inside a Promise. For instance, given Promise<string>, this type will produce string.

The combinePromises Function

This function aims to execute multiple asynchronous operations in parallel and return their results in an aggregated object.

  • Each property of the object is expected to represent a Promise of an asynchronous operation.

  • We employ Promise.all to execute all asynchronous operations in parallel.

  • Eventually, the resultant object is returned.

Sample Functions

  • fetchUser, fetchComments, and fetchCompany are functions that simulate asynchronous operations.

  • Currently, fetchUser is set to return an error, but you can uncomment a line to test the successful scenario.

  • Similarly, fetchComments and fetchCompany have both successful and unsuccessful cases, but the unsuccessful scenarios are commented out for now.

Conclusion

The provided code illustrates a pattern of handling multiple asynchronous operations in a type-safe manner. By leveraging TypeScript's advanced type features, you can write error-handling logic that's both safe and concise.

Did you find this article valuable?

Support Higashi Kota by becoming a sponsor. Any amount is appreciated!