Random UI

Handled Response

Utility snippet to handle responses with type safety and readability

TypeScript
TypeScript

Philosophy

This utility snippet is particularly useful to handle responses from API calls, server actions, or any other operations that can return success or failure. It has several benefits:

  • Type safety: It enforces type safety and readability. It also benefits from generic types.
  • Readability: It's more readable than the traditional try/catch block.
  • Flexibility: You can define data type and error type separately, or use the same type for both.
  • Pattern matching: It enhances Single Responsibility Principle (SRP) by separating the logic of the response from the logic of the request, letting you handle the response as you want.

Installation

Copy and paste the following code into your project.

lib/handled-response.ts
export type HandledResponse<D, E = D> =
  | {
      data: D;
      error?: never;
    }
  | {
      data?: never;
      error?: E;
    };

export const ok = <D, E = D>(data: D): HandledResponse<D, E> => ({
  data,
});

export const err = <D, E = D>(error: E): HandledResponse<D, E> => ({
  error,
});

Usage

import { HandledResponse, ok, err } from "@/lib/handled-response";

Same return type

import { HandledResponse, ok, err } from "@/lib/handled-response";

// Imagine this is your API call or database query
async function getPostMessageCrud(postId: string): Promise<HandledResponse<string>> {
  try {
    const post = await db.findById(postId);
    if (!post) {
      return err("Post not found"); // Automatically narrows the type to { error: string }
    }
    return ok(post.message); // Automatically narrows the type to { data: string }
  }
}

Then

async function getPostMessage(postId: string) {
  const { error, data } = await getPostMessageCrud(postId);

  data?.name; // Error: data is never, does not exist

  if (error) {
    // Type narrowed: error is string
    console.error("Failed to fetch user:", error);
    return;
  }
  
  // Type narrowed: data is string
  console.log("Post message found:", data);
}

Different return types

Suppose you have a function that fetches user data from a database or an API. You want to strictly handle the possible outcomes (success or error) with type safety.

import { HandledResponse, ok, err } from "@/lib/handled-response";

// Imagine this is your API call or database query
async function fetchUserById(userId: string): Promise<HandledResponse<User,string>> {
  try {
    const user = await db.findUser(userId);
    if (!user) {
      return err("User not found"); // Automatically narrows the type to { error: string }
    }
    return ok(user); // Automatically narrows the type to { data: User }
  } catch (error) {
    return err("Unexpected error");
  }
}

You can then use the returned object in an exhaustive way and typeScript will enforce that you check both possible states:

async function handleUserRequest(userId: string) {
  const { error, data } = await fetchUserById(userId);


  data?.name; // Error: data is never, does not exist

  if (error) {
    // Type narrowed: error is string
    console.error("Failed to fetch user:", error);
    return;
  }

  // Type narrowed: data is User
  console.log("User found:", data.name, data.email);
}