Error Handling
Error handling is the cornerstone of the HttpStream / WebSocketStream pipeline. Every step in a pipeline evaluates to a Result — either a success that continues the pipeline or an error that short-circuits it. There are no exception-based control flow paths in handler code: you either return a success, return an error, or let an unexpected exception bubble out of the handler (which the framework translates into a 500 and hands to your configured exception handler).
The Result Contract
Section titled “The Result Contract”Pipeline steps on HttpStream evaluate to Result<HttpErrorResponse, T>. An HttpErrorResponse pairs an ErrorMessageResponse with a status code, and it is always what the client sees when a pipeline errors:
public record ErrorMessageResponse(String message, Map<String, List<String>> errors) { }public record HttpErrorResponse(ErrorMessageResponse errorMessageValue, int statusCode) { }Any error you produce must supply:
- A status code (
ErrorStatusCodeenum —BAD_REQUEST,NOT_FOUND,UNPROCESSABLE_ENTITY, etc.) - An
ErrorMessageResponsewith a required top-levelmessageand an optional map of field-level messages
Build one with HttpResult.error(...):
// Just a messageHttpResult.error( ErrorStatusCode.NOT_FOUND, new ErrorMessageResponse("User not found"));
// Message plus field-level detailsHttpResult.error( ErrorStatusCode.UNPROCESSABLE_ENTITY, new ErrorMessageResponse("Validation failed", Map.of( "email", List.of("must be a valid email address"), "age", List.of("must be at least 0"))));The response body is the serialized ErrorMessageResponse:
{ "message": "Validation failed", "errors": { "email": ["must be a valid email address"], "age": ["must be at least 0"] }}When the errors map is empty (the single-argument ErrorMessageResponse constructor) the field is still serialized, just as {}.
Which Steps Can Return Errors
Section titled “Which Steps Can Return Errors”Not every pipeline step is allowed to short-circuit. Only the flatMap-style steps and the complete terminators take a callback returning a Result:
| Step | Signature | Can return an error? |
|---|---|---|
map | ctx -> OUT | No — returns a plain value |
blockingMap | ctx -> OUT | No |
peek / blockingPeek | ctx -> void | No |
flatMap | ctx -> Result<HttpErrorResponse, OUT> | Yes |
blockingFlatMap | ctx -> Result<HttpErrorResponse, OUT> | Yes |
asyncMap / asyncBlockingMap | ctx -> LuxisAsync<OUT> | Yes (via the async result) |
complete | ctx -> Result<HttpErrorResponse, OUT> | Yes |
blockingComplete | ctx -> Result<HttpErrorResponse, OUT> | Yes |
If a step that can return an error does so, the remainder of the pipeline is skipped and the error response is written to the client immediately.
return e .validate(v -> v.field("id", r -> r.id).required()) .blockingFlatMap(ctx -> { final UserRecord user = database.findById(ctx.in().id()); if (user == null) { return HttpResult.error( ErrorStatusCode.NOT_FOUND, new ErrorMessageResponse("User not found")); } return HttpResult.success(user); }) .map(ctx -> toUserResponse(ctx.in())) .complete(ctx -> HttpResult.success(ctx.in()));map cannot short-circuit because its signature returns OUT directly, not a Result. If you need to decide between success and error mid-pipeline, use flatMap (or blockingFlatMap).
Built-in Error Producers
Section titled “Built-in Error Producers”A few framework features already produce error results for you — they use the same ErrorMessageResponse contract, so clients see a consistent shape regardless of where the error came from:
validate()— short-circuits with422 Unprocessable Entityand anerrorsmap populated from the failing field rules.requireJwt()— short-circuits with401 Unauthorizedand a message describing the failure (Missing or invalid Authorization header,Invalid token signature,Token has expired, …).- JSON body deserialization — if the request body is missing or cannot be parsed, the pipeline short-circuits with
400 Bad Requestand{"message":"Invalid json request"}.
Unexpected Exceptions
Section titled “Unexpected Exceptions”Any exception thrown out of a pipeline step that is not wrapped in a Result is treated as a bug. The framework will:
- Stop the pipeline.
- Respond to the client with
500 Internal Server Errorand body{"message":"Something went wrong"}. - Invoke the configured exception handler with the thrown exception.
The exception handler is for observability only — logging, metrics, alerting. It does not change the response. Register it on the server config:
final WebServerConfig config = new WebServiceConfigBuilder() .setPort(8080) .setExceptionHandler(e -> logger.error("Unhandled pipeline exception", e)) .build();Because exceptions become opaque 500s, prefer returning HttpResult.error(...) for any failure you want the client to see a meaningful message for. Reserve thrown exceptions for genuinely unexpected conditions (programmer errors, bugs, infrastructure outages).
// Preferred — the client gets a 404 and a useful message.blockingFlatMap(ctx -> { try { return HttpResult.success(database.findById(ctx.in().id())); } catch (NotFoundException e) { return HttpResult.error( ErrorStatusCode.NOT_FOUND, new ErrorMessageResponse("User not found")); }})
// Dangerous — the client gets 500 "Something went wrong".blockingMap(ctx -> database.findById(ctx.in().id())) // throws on missingWebSocket Errors
Section titled “WebSocket Errors”WebSocketStream follows exactly the same pattern, with one difference: WebSocket frames have no HTTP status code, so errors are just an ErrorMessageResponse — no HttpErrorResponse wrapper. Build errors with WebSocketResult:
return e .validate(v -> v.field("value", r -> r.value).required().min(0)) .flatMap(ctx -> { if (ctx.in().value() > 100) { return WebSocketResult.error("value must be at most 100"); } return WebSocketResult.success(ctx.in()); }) .complete(ctx -> WebSocketResult.success(process(ctx.in())));Errors are delivered to the client as a framed error message ({"type":"error","payload":{...}}) whose payload is the serialized ErrorMessageResponse. Unexpected exceptions on a WebSocket pipeline produce the same {"message":"Something went wrong","errors":{}} payload and invoke the exception handler, exactly as on the HTTP side.