How we turned opaque Internal Server Errors into user-friendly feedback in our NestJS + React stack.
There is nothing more frustrating than a generic “500 Internal Server Error” message. It’s the digital equivalent of a shrug. Today, we’re sharing how we debugged a persistent 500 error in our invoice fiscalization flow and built a robust error handling system to banish them for good.
The Ghost in the Machine
It started with a simple feature: previewing an invoice before sending it to the tax authority.
Our frontend sent a payload to the backend, and the backend responded with 500 Internal Server Error.
No logs, no stack trace in the client, just silence.
We dug into the backend logs and found… nothing useful. The request just seemed to die.
The Problem: Streams and Validation
After hours of peeling back layers, we found two culprits working together:
- Premature Stream Consumption: We had added debug logging in our proxy layer (
api.resource.ts) to print the request body. Critically, reading the request stream for logging meant it was already “used up” before it reached the actual API destination. - Silent Validation Failures: Our NestJS backend uses
class-validatorDTOs. When we fixed the stream issue, we hit another wall. The request was empty because the strictValidationPipewas stripping out properties that didn’t have decorators. Instead of a helpful “Field X is missing”, it just crashed with an empty object.
The Solution: Standardize Everything
Once we fixed the bugs (removing the logger and adding decorators), we realized our UX was still fragile. If the API failed, the frontend would crash or show a blank screen. We needed a contract.
1. The ApiError Contract
We defined a standard error shape that the frontend can always expect:
interface ApiErrorInterface {
status: number; // 400, 401, 500
title: string; // "Validation Failed"
message: string; // "Email is invalid"
details?: string[]; // ["password too short", "email invalid"]
}
2. The Universal Wrapper
We built a handleApiRequest utility that wraps every single API call:
// api.server.ts
export async function handleApiRequest<T>(promise: Promise<T>) {
try {
const data = await promise;
return { success: true, data };
} catch (error) {
// Automatically parses NestJS validation arrays into strings
// Transforms network errors into clean JSON
return { success: false, error: parseApiError(error) };
}
}
The Result
Now, when a user makes a mistake (like entering an invalid Tax ID), they don’t see a spinner that hangs forever or a console error. They see this:
Validation Failed (400)
❌ buyerTin must be a valid TIN string
By establishing a strict error contract and handling it centrally, we turned a debugging nightmare into a feature that actively guides the user.
Building complex financial integrations? Check out the TaxBridge Documentation to see how we handle the hard stuff for you.