Error Handling
Proper error handling is crucial for building robust and reliable web applications. Horse provides a structured approach to handling exceptions and errors through specialized exception classes and middleware patterns.
Exception Types
Horse provides two primary exception types for controlling application flow and error responses.
EHorseException
EHorseException is the main exception class for handling HTTP errors with detailed context and structured error responses.
Basic Usage
uses
Horse, Horse.Exception;
THorse.Get('/users/:id',
procedure(Req: THorseRequest; Res: THorseResponse; Next: TNextProc)
var
LUserId: Integer;
begin
LUserId := StrToIntDef(Req.Params['id'], -1);
if LUserId <= 0 then
raise EHorseException.New
.Error('Invalid user ID')
.Status(THTTPStatus.BadRequest);
// Continue processing...
end);
Creating Detailed Exceptions
raise EHorseException.New
.Error('User not found')
.Status(THTTPStatus.NotFound)
.Title('Resource Not Found')
.Detail('The requested user does not exist in the database')
.Code(1001)
.&Type(TMessageType.Error)
.Hint('Please verify the user ID and try again')
.&Unit('UserController');
The & prefix on Type and Unit is required because these are reserved words in Delphi. This is a special syntax that allows using reserved words as identifiers.
Exception Properties
Property Type Description Errorstring Main error message StatusTHTTPStatus HTTP status code Titlestring Error title for structured responses Detailstring Detailed error description CodeInteger Application-specific error code TypeTMessageType Message type (Error, Warning, Information) Hintstring Suggestion for resolving the error Unitstring Source unit/module where error occurred
JSON Error Response
EHorseException can automatically generate JSON error responses:
var
LException: EHorseException;
LJson: string;
begin
LException := EHorseException.New
.Error('Validation failed')
.Status(THTTPStatus.BadRequest)
.Title('Invalid Input')
.Code(4001)
.Hint('Check your input parameters');
LJson := LException.ToJSON;
// Returns:
// {
// "title": "Invalid Input",
// "code": 4001,
// "error": "Validation failed",
// "hint": "Check your input parameters"
// }
end;
EHorseCallbackInterrupted
EHorseCallbackInterrupted is used to interrupt the request processing chain without triggering error handlers. This is useful for early exits in middleware or when you’ve already sent a response.
uses
Horse, Horse.Exception.Interrupted;
procedure AuthMiddleware(Req: THorseRequest; Res: THorseResponse; Next: TNextProc);
var
LToken: string;
begin
LToken := Req.Headers['Authorization'];
if LToken = '' then
begin
Res.Status(THTTPStatus.Unauthorized).Send('Missing authorization token');
raise EHorseCallbackInterrupted.Create; // Stop processing, don't call Next()
end;
if not ValidateToken(LToken) then
begin
Res.Status(THTTPStatus.Forbidden).Send('Invalid token');
raise EHorseCallbackInterrupted.Create('Token validation failed');
end;
Next(); // Continue to next handler
end;
Always send a response before raising EHorseCallbackInterrupted. This exception interrupts the chain but doesn’t automatically send any response to the client.
When to Use EHorseCallbackInterrupted
Authentication/Authorization failures : When you’ve already sent an auth error response
Early validation exits : When validation fails in middleware
Rate limiting : When request quota is exceeded
Conditional processing : When certain conditions prevent further processing
Error Handling Patterns
Try-Except in Routes
The most basic error handling approach:
THorse.Post('/users',
procedure(Req: THorseRequest; Res: THorseResponse; Next: TNextProc)
var
LUser: TUser;
begin
try
LUser := TUser.Create;
try
LUser.Name := Req.Body<TJSONObject>.GetValue<string>('name');
LUser.Email := Req.Body<TJSONObject>.GetValue<string>('email');
if not LUser.Validate then
raise EHorseException.New
.Error('Validation failed')
.Status(THTTPStatus.BadRequest);
LUser.Save;
Res.Status(THTTPStatus.Created).Send('User created');
finally
LUser.Free;
end;
except
on E: EHorseException do
Res.Status(E.Status).Send(E.ToJSON);
on E: Exception do
Res.Status(THTTPStatus.InternalServerError)
.Send('Internal error: ' + E.Message);
end;
end);
Global Error Handler Middleware
Create centralized error handling:
procedure ErrorHandlerMiddleware(Req: THorseRequest; Res: THorseResponse; Next: TNextProc);
begin
try
Next(); // Call next handler in chain
except
on E: EHorseException do
begin
Res.Status(E.Status)
.ContentType('application/json')
.Send(E.ToJSON);
end;
on E: EHorseCallbackInterrupted do
begin
// Request was interrupted intentionally, do nothing
// Response should already be sent
end;
on E: Exception do
begin
// Log unexpected errors
LogError(E.Message, E.StackTrace);
Res.Status(THTTPStatus.InternalServerError)
.Send('An unexpected error occurred');
end;
end;
end;
begin
// Apply error handler as first middleware
THorse.Use(ErrorHandlerMiddleware);
// Define routes...
THorse.Listen(9000);
end.
Place your error handler middleware first in the chain so it can wrap all subsequent handlers and catch any exceptions they raise.
Validation Helper Pattern
Complete Validation Helper Example
unit MyApp.Validation;
interface
uses
System.SysUtils, System.JSON, Horse, Horse.Exception;
type
TValidator = class
public
class procedure RequireField(AJson: TJSONObject; const AFieldName: string);
class procedure RequireEmail(const AEmail: string);
class procedure RequireMinLength(const AValue: string; AMinLength: Integer; const AFieldName: string);
class procedure RequirePositive(AValue: Integer; const AFieldName: string);
class procedure RequireInRange(AValue: Integer; AMin, AMax: Integer; const AFieldName: string);
end;
implementation
class procedure TValidator.RequireField(AJson: TJSONObject; const AFieldName: string);
begin
if not Assigned(AJson.GetValue(AFieldName)) then
raise EHorseException.New
.Error(Format('Field "%s" is required', [AFieldName]))
.Status(THTTPStatus.BadRequest)
.Title('Missing Required Field')
.Code(4001);
end;
class procedure TValidator.RequireEmail(const AEmail: string);
begin
if not AEmail.Contains('@') then
raise EHorseException.New
.Error('Invalid email address')
.Status(THTTPStatus.BadRequest)
.Title('Validation Error')
.Detail(Format('The email "%s" is not valid', [AEmail]))
.Hint('Email must contain @ symbol')
.Code(4002);
end;
class procedure TValidator.RequireMinLength(const AValue: string; AMinLength: Integer; const AFieldName: string);
begin
if Length(AValue) < AMinLength then
raise EHorseException.New
.Error(Format('%s must be at least %d characters', [AFieldName, AMinLength]))
.Status(THTTPStatus.BadRequest)
.Code(4003);
end;
class procedure TValidator.RequirePositive(AValue: Integer; const AFieldName: string);
begin
if AValue <= 0 then
raise EHorseException.New
.Error(Format('%s must be a positive number', [AFieldName]))
.Status(THTTPStatus.BadRequest)
.Code(4004);
end;
class procedure TValidator.RequireInRange(AValue: Integer; AMin, AMax: Integer; const AFieldName: string);
begin
if (AValue < AMin) or (AValue > AMax) then
raise EHorseException.New
.Error(Format('%s must be between %d and %d', [AFieldName, AMin, AMax]))
.Status(THTTPStatus.BadRequest)
.Code(4005);
end;
end.
Usage: THorse.Post('/api/users',
procedure(Req: THorseRequest; Res: THorseResponse; Next: TNextProc)
var
LJson: TJSONObject;
LName, LEmail: string;
LAge: Integer;
begin
LJson := Req.Body<TJSONObject>;
// Validate required fields
TValidator.RequireField(LJson, 'name');
TValidator.RequireField(LJson, 'email');
TValidator.RequireField(LJson, 'age');
LName := LJson.GetValue<string>('name');
LEmail := LJson.GetValue<string>('email');
LAge := LJson.GetValue<Integer>('age');
// Validate field values
TValidator.RequireMinLength(LName, 3, 'Name');
TValidator.RequireEmail(LEmail);
TValidator.RequireInRange(LAge, 18, 120, 'Age');
// All validations passed, create user
CreateUser(LName, LEmail, LAge);
Res.Status(THTTPStatus.Created).Send('User created');
end);
HTTP Status Codes
Horse provides comprehensive HTTP status code support through the THTTPStatus enum:
Common Status Codes
// Success
Res.Status(THTTPStatus.OK); // 200
Res.Status(THTTPStatus.Created); // 201
Res.Status(THTTPStatus.NoContent); // 204
// Redirection
Res.Status(THTTPStatus.MovedPermanently); // 301
Res.Status(THTTPStatus.Found); // 302
Res.Status(THTTPStatus.SeeOther); // 303
// Client Errors
Res.Status(THTTPStatus.BadRequest); // 400
Res.Status(THTTPStatus.Unauthorized); // 401
Res.Status(THTTPStatus.Forbidden); // 403
Res.Status(THTTPStatus.NotFound); // 404
Res.Status(THTTPStatus.MethodNotAllowed); // 405
Res.Status(THTTPStatus.Conflict); // 409
Res.Status(THTTPStatus.UnprocessableEntity); // 422
Res.Status(THTTPStatus.TooManyRequests); // 429
// Server Errors
Res.Status(THTTPStatus.InternalServerError); // 500
Res.Status(THTTPStatus.NotImplemented); // 501
Res.Status(THTTPStatus.ServiceUnavailable); // 503
Status Code Best Practices
Use appropriate status codes
Choose status codes that accurately represent the response:
2xx : Success
3xx : Redirection
4xx : Client errors (user’s fault)
5xx : Server errors (server’s fault)
Be consistent
Use the same status codes for similar situations across your API.
Include error details
Don’t just send a status code - include a descriptive error message.
Advanced Error Handling
type
TErrorResponse = record
Success: Boolean;
ErrorCode: Integer;
Message: string;
Timestamp: TDateTime;
Path: string;
class function Create(ACode: Integer; const AMessage, APath: string): TErrorResponse; static;
function ToJSON: string;
end;
class function TErrorResponse.Create(ACode: Integer; const AMessage, APath: string): TErrorResponse;
begin
Result.Success := False;
Result.ErrorCode := ACode;
Result.Message := AMessage;
Result.Timestamp := Now;
Result.Path := APath;
end;
function TErrorResponse.ToJSON: string;
var
LJson: TJSONObject;
begin
LJson := TJSONObject.Create;
try
LJson.AddPair('success', TJSONBool.Create(Success));
LJson.AddPair('errorCode', TJSONNumber.Create(ErrorCode));
LJson.AddPair('message', Message);
LJson.AddPair('timestamp', DateTimeToStr(Timestamp));
LJson.AddPair('path', Path);
Result := LJson.ToJSON;
finally
LJson.Free;
end;
end;
// Usage
procedure ErrorMiddleware(Req: THorseRequest; Res: THorseResponse; Next: TNextProc);
var
LError: TErrorResponse;
begin
try
Next();
except
on E: EHorseException do
begin
LError := TErrorResponse.Create(
E.Code,
E.Error,
Req.PathInfo
);
Res.Status(E.Status)
.ContentType('application/json')
.Send(LError.ToJSON);
end;
end;
end;
Logging Errors
uses
System.SysUtils, System.IOUtils;
procedure LogError(const AMessage, AStackTrace: string);
var
LLogFile: string;
LLogEntry: string;
begin
LLogFile := TPath.Combine(TPath.GetDocumentsPath, 'app_errors.log');
LLogEntry := Format('[%s] ERROR: %s%s%s%s',
[FormatDateTime('yyyy-mm-dd hh:nn:ss', Now),
AMessage,
sLineBreak,
AStackTrace,
sLineBreak + sLineBreak]);
TFile.AppendAllText(LLogFile, LLogEntry, TEncoding.UTF8);
end;
procedure ErrorLoggerMiddleware(Req: THorseRequest; Res: THorseResponse; Next: TNextProc);
begin
try
Next();
except
on E: Exception do
begin
LogError(
Format('%s - %s %s', [E.ClassName, Req.MethodType.ToString, Req.PathInfo]),
E.Message
);
raise; // Re-raise to let other error handlers process it
end;
end;
end;
Database Transaction Rollback
THorse.Post('/api/orders',
procedure(Req: THorseRequest; Res: THorseResponse; Next: TNextProc)
var
LConnection: TFDConnection;
begin
LConnection := GetDatabaseConnection;
LConnection.StartTransaction;
try
// Perform database operations
CreateOrder(Req.Body<TJSONObject>);
UpdateInventory(Req.Body<TJSONObject>);
SendConfirmationEmail(Req.Body<TJSONObject>);
LConnection.Commit;
Res.Status(THTTPStatus.Created).Send('Order created');
except
on E: Exception do
begin
LConnection.Rollback;
if E is EHorseException then
raise
else
raise EHorseException.New
.Error('Failed to create order')
.Status(THTTPStatus.InternalServerError)
.Detail(E.Message);
end;
end;
end);
Error Handling Best Practices
Use specific exception types
Use EHorseException for expected errors with proper status codes. Reserve generic exceptions for truly unexpected errors.
Provide helpful error messages
Include enough context for clients to understand and fix the problem, but don’t expose sensitive system details.
Log server errors
Always log 5xx errors with full stack traces for debugging, but send sanitized messages to clients.
Handle resources properly
Always use try-finally blocks to ensure resources (database connections, files, etc.) are properly cleaned up.
Don't catch everything
Let unexpected exceptions bubble up to your global error handler. Don’t hide errors by catching and ignoring them.
Security Notice : Never expose sensitive information in error messages:
Database connection strings
File paths
Stack traces (in production)
Internal system details
Testing Error Handlers
// Test helper to simulate errors
THorse.Get('/test/error/:type',
procedure(Req: THorseRequest; Res: THorseResponse; Next: TNextProc)
var
LErrorType: string;
begin
LErrorType := Req.Params['type'];
case IndexStr(LErrorType, ['badrequest', 'notfound', 'servererror']) of
0: raise EHorseException.New
.Error('Bad request test')
.Status(THTTPStatus.BadRequest);
1: raise EHorseException.New
.Error('Not found test')
.Status(THTTPStatus.NotFound);
2: raise Exception.Create('Server error test');
else
Res.Send('Unknown error type');
end;
end);
API Reference
EHorseException Methods
Method Description NewCreate a new exception instance Error(value)Set error message Status(value)Set HTTP status code Title(value)Set error title Detail(value)Set detailed description Code(value)Set application error code Type(value)Set message type Hint(value)Set hint for resolution Unit(value)Set source unit name ToJSONConvert to JSON string ToJSONObjectConvert to TJSONObject
EHorseCallbackInterrupted
Constructor Description CreateCreate without message Create(message)Create with custom message