Handling Try-Catch in API Endpoints with the Result Pattern

API DevelopmentTypeScript

When building API endpoints, handling errors and exceptions is a crucial aspect of ensuring a robust and maintainable application. One common approach is to use try-catch blocks to catch and handle errors. However, this approach can lead to cluttered code and make it difficult to handle errors consistently across the application. In this post, we'll explore how to use the Result pattern to handle try-catch in API endpoints, providing a more elegant and scalable solution.

What is the Result Pattern?

The Result pattern is a software design pattern that returns an object containing the outcome of an operation, along with any data it returned. This pattern allows for a more explicit and consistent way of handling errors and exceptions, making code more robust and maintainable.

Implementing the Result Pattern in API Endpoints

Here's an example of how to implement the Result pattern in an API endpoint using TypeScript:

export type ApiError = "InvalidJson" | "RequestFailed" | "NetworkError";

export type Result<T, E extends ApiError> =
  | { ok: true; data: T }
  | { ok: false; error: E };

async function getResult<T>(
  apiCall: () => Promise<Response>
): Promise<Result<T, ApiError>> {
  try {
    const response = await apiCall();
    if (!response.ok) throw new Error("Not OK!");
    try {
      const data = await response.json() ;
      return { ok: true, data };
    } catch (jsonError) {
      return { ok: false, error: "InvalidJson" };
    }
  } catch (error) {
    return { ok: false, error: "RequestFailed" };
  }
}

In this example, the getResult function returns a Result object, the result is a discriminated union the discriminator key ok that can be used to safely check if the api call was successful.

Handling Errors with the Result Pattern

interface Todo {
  title: string
}

async function getTodo(id: number) {
  const result = await getResult<Todo>(() => fetch(`https://api.example.com/todos/${id}`))

  if (result.ok) {
    console.log(`Todo: ${result.data.title}`)
  }
  else {
    // Handle specific errors
    switch (result.error) {
      case 'InvalidJson':
        console.error('Error parsing JSON response')
        break
      case 'RequestFailed':
        console.error('API request failed')
        break
      default:
        console.error('Unknown error')
    }
  }
}

The getResult function uses the Result pattern to handle errors in a consistent and explicit way. Using the ok property we're able to get type safety access to the data or error property. This means for each api call if we want to access the data property we first need to check if the result has the ok property set to true

Benefits of the Result Pattern

The Result pattern provides a powerful and flexible way to handle try-catch in API endpoints, making it easier to write robust and maintainable code. By using the Result pattern, you can:

  • Separate error handling logic from the main code flow, making it easier to read and understand
  • Handle different types of errors and exceptions in a flexible and scalable way
  • Write more robust and maintainable code

Conclusion

In this post, we've explored how to use the Result pattern to handle try-catch in API endpoints. By using the Result pattern, you can write more robust and maintainable code that is easier to read and understand. Remember to separate error handling logic from the main code flow and handle different types of errors and exceptions in a flexible and scalable way.