Handled Response
Utility snippet to handle responses with type safety and readability

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.
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);
}