https://a.storyblok.com/f/283157/1920x800/18e18690e6/detailed_error.jpg

Web error handling concept

Published: Oct 6, 2024
• • •

Handling errors in the backend

In this architecture, I always add a class or structure, which I often call "Detailed Error". This contains an exception or error (depending on your programming language), but you can add additional details to the error. You can also add frontend information, like a message or a code.

When no frontend information is set, the message will be set to "Internal server error" and the code to, e.g., "ERROR_INTERNAL_SERVER_ERROR".

Here is an example of how it could look like:

// A pseudo code example in Go
func serviceFunction(input string) error {
	if input == "" {
		// This would create an error 400 with the given error details
		return &DetailedError{
			Details: "The input for serviceFunction was empty",
			FrontendErrorCode: "ERROR_INPUT_EMPTY",
			FrontendErrorMessage: "The field input is empty",
			FrontendStatus: http.StatusBadRequest,
		}
	}

	err := doSomethingInDatabase()
	if err != nil {
		// This would create an error 500 with the default internal server error
		return &DetailedError{
			InnerError: err,
			Details: "Error occurred when calling doSomethingInDatabase in serviceFunction",
		}
	}
	
	return nil
}

func requestHandler(w http.ResponseWriter, r *http.Request) {
	err := serviceFunction(readInputFromRequest(r))
	if err != nil {
		// A function, which walks down the inner errors of the
		// detailed error and creates a log entry containing all
		// information for optimal debugging
		logError(err)
		
		// Send a response depending on the frontend error information
		// set in the detailed error object.
		sendErrorResponse(w, err)
		return
	}
	
	sendSuccessResponse(w)
}

Or if you prefer TypeScript:

// A pseudo code example in TS
function serviceFunction(input: string) {
	if (!input) {
		throw new DetailedError()
			.addDetails("The input for serviceFunction was empty")
			.setFrontendErrorCode("ERROR_INPUT_EMPTY")
			.setFrontendErrorMessage("The field input is empty")
			.setFrontendStatus(400)
	}
	
	try {
		doSomethingInDatabase()
	} catch (e: unknown) {
		throw new DetailedError()
			.setInnerError(e)
			.addDetails("Error occurred when calling doSomethingInDatabase in serviceFunction")
	}
}

function requestHandler(req: Request, res: Response) {
	try {
		serviceFunction(readInputFromRequest(req))
		sendSuccessResponse(res)
	} catch (e: unknown) {
		// A function, which walks down the inner errors of the
		// detailed error and creates a log entry containing all
		// information for optimal debugging
		logError(err)
		
		// Send a response depending on the frontend error information
		// set in the detailed error object.
		sendErrorResponse(res, e)
	}
}

You have to replace the function "logError" with a function that walks down the DetailedError object (because they can nest). You could, for example, create a string array containing all added details or with the error parsed to a string when it is not a DetailedError. Then write the result formatted to the console or to your logger.

Then you need to implement a function to write a response depending on the error. This function should try to find a DetailedError instance that contains information for the frontend and build a response, or send the default server error response when it can't find any.