Propagating API Errors Gracefully in .NET Controllers

When building an API, a common scenario is making outbound API calls within your controller’s logic and handling errors appropriately. One of the more frequent challenges is when the external API returns a 404 (Not Found), and we want to ensure this error is propagated back to the client calling our controller. In this post, I’ll walk through a clean, maintainable way to handle this situation in .NET 8 using modern patterns.

Thank me by sharing on Twitter 🙏

When you’re building an API, maintaining clarity in error handling is key to ensuring a great developer experience for both your team and your users. In this post, I’ll share my approach for handling outbound API errors—particularly 404s—and how to structure your code to propagate them properly.

Breaking Down the Scenario

Imagine you have a .NET 8 controller that needs to fetch data from a third-party API. If that API returns a 404, you want to make sure that your controller also returns a 404 to the client calling your route, instead of generic error responses or exceptions that might confuse the client.

To solve this, we need to:

  1. Make the outbound call using HttpClient.
  2. Handle the potential 404 response from the third-party service.
  3. Propagate that error back to our controller.
  4. Return a well-structured 404 response from the controller.

Structuring the Service Layer

I usually start by structuring the service layer, which handles the outbound API calls. Here, I’m using the built-in HttpClient. It’s important that the service not only makes the call but also interprets the response correctly and throws an appropriate exception when the resource is not found.

Here’s what this looks like in TypeScript, which mirrors how you’d do it in .NET 8:

C#
class MyApiService {
    private httpClient: HttpClient;

    constructor(httpClient: HttpClient) {
        this.httpClient = httpClient;
    }

    async getData(id: string): Promise<MyDataDto> {
        const response = await this.httpClient.get(`https://api.example.com/data/${id}`);

        if (response.status === 404) {
            // Throw a custom error if the resource is not found
            throw new ResourceNotFoundException("Data not found");
        }

        if (!response.ok) {
            // Handle other error scenarios
            throw new Error("Something went wrong while fetching the data");
        }

        return response.json();
    }
}

// Custom exception class to indicate a 404 error
class ResourceNotFoundException extends Error {
    constructor(message: string) {
        super(message);
        this.name = "ResourceNotFoundException";
    }
}

This code sets up a MyApiService class that performs the API call. Notice how it checks for a 404 response and throws a custom exception, ResourceNotFoundException. This makes our error handling more expressive and keeps our service logic clean.

Propagating Errors in the Controller

Next, the controller needs to catch the specific error and respond accordingly. This allows the client calling your API to receive a 404 status code when the resource isn’t found, just as if they were interacting directly with the third-party API.

Here’s how I approach this in the controller:

C#
@Controller('api/data')
export class MyController {
    constructor(private readonly myApiService: MyApiService) {}

    @Get(':id')
    async getData(@Param('id') id: string): Promise<MyDataDto> {
        try {
            const result = await this.myApiService.getData(id);
            return result;
        } catch (error) {
            if (error instanceof ResourceNotFoundException) {
                throw new HttpException('Resource not found', HttpStatus.NOT_FOUND);
            }

            throw new HttpException('Internal Server Error', HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

In this controller, I’m catching the ResourceNotFoundException thrown by the service and mapping it to a 404 HTTP status code. For any other unhandled exceptions, I’m returning a 500 status code to indicate a general server error.

By structuring the controller in this way, the error flow is clean: when the service detects a missing resource, it triggers the correct exception, and the controller translates that into a response suitable for the client.

Advantages of This Approach

  1. Clarity and Separation of Concerns: By handling the 404 logic in the service layer and propagating the error to the controller, we maintain a clean separation of concerns. Each layer does what it’s supposed to do without leaking unnecessary details to other parts of the codebase.
  2. Custom Exceptions for Better Error Handling: Introducing custom exceptions like ResourceNotFoundException keeps our code expressive and easier to maintain. It’s clear what’s happening and why, which benefits other developers reading or maintaining your code.
  3. Consistent API Responses: Propagating the correct status codes ensures that the clients of your API receive consistent and meaningful responses, making the API easier to work with.

Wrapping Up

Handling outbound API calls and propagating errors correctly is crucial for building reliable and intuitive APIs. By leveraging a service layer that handles the outbound API logic and using custom exceptions, you can maintain clean, maintainable code while giving your clients the correct feedback when things go wrong.

The approach I’ve shared here is easy to extend for more complex scenarios, and it scales well as your application grows. Remember, a well-structured error-handling flow not only improves your API’s reliability but also makes life easier for everyone interacting with your endpoints.

This strategy has worked well for me, and I hope you find it useful in your own .NET 8 projects.

Share this:

Leave a Reply