Result ADT
Typr Events generates Result ADTs (Algebraic Data Types) for RPC methods, providing type-safe error handling.
The Problemβ
Traditional RPC error handling is error-prone:
// Checked exceptions - clutters code
User getUser(String id) throws UserNotFoundException;
// Unchecked exceptions - easy to forget handling
User getUser(String id); // might throw!
// Null returns - loses error information
User getUser(String id); // null means... what?
The Solutionβ
Result ADT makes errors explicit in the type system:
GetUserResult getUser(String userId);
Generated Result Typesβ
For a method with errors:
{
"response": "User",
"errors": ["UserNotFoundError"]
}
Javaβ
public sealed interface GetUserResult
permits GetUserResult.Ok, GetUserResult.Err {
record Ok(User value) implements GetUserResult {}
record Err(UserNotFoundError error) implements GetUserResult {}
}
Kotlinβ
sealed interface GetUserResult {
data class Ok(val value: User) : GetUserResult
data class Err(val error: UserNotFoundError) : GetUserResult
}
Scalaβ
enum GetUserResult:
case Ok(value: User)
case Err(error: UserNotFoundError)
Pattern Matchingβ
Javaβ
var result = userService.getUser(userId);
switch (result) {
case GetUserResult.Ok(var user) -> {
return ResponseEntity.ok(user);
}
case GetUserResult.Err(var error) -> {
return ResponseEntity.notFound()
.body(error.message());
}
}
Kotlinβ
when (val result = userService.getUser(userId)) {
is GetUserResult.Ok -> ResponseEntity.ok(result.value)
is GetUserResult.Err -> ResponseEntity.notFound()
.body(result.error.message)
}
Scalaβ
userService.getUser(userId) match
case GetUserResult.Ok(user) => Ok(user)
case GetUserResult.Err(error) => NotFound(error.message)
Multiple Error Typesβ
Methods can have multiple error types:
{
"response": "User",
"errors": ["UserNotFoundError", "ValidationError"]
}
Generatedβ
public sealed interface CreateUserResult
permits CreateUserResult.Ok,
CreateUserResult.UserNotFoundErr,
CreateUserResult.ValidationErr {
record Ok(User value) implements CreateUserResult {}
record UserNotFoundErr(UserNotFoundError error) implements CreateUserResult {}
record ValidationErr(ValidationError error) implements CreateUserResult {}
}
Handlingβ
switch (result) {
case CreateUserResult.Ok(var user) -> success(user);
case CreateUserResult.UserNotFoundErr(var e) -> notFound(e);
case CreateUserResult.ValidationErr(var e) -> badRequest(e);
}
Void Resultsβ
For methods with "response": "null":
public sealed interface DeleteUserResult
permits DeleteUserResult.Ok, DeleteUserResult.Err {
record Ok() implements DeleteUserResult {}
record Err(UserNotFoundError error) implements DeleteUserResult {}
}
Benefitsβ
- Compile-time safety - Can't forget to handle errors
- Exhaustive matching - Compiler warns if you miss a case
- No exceptions - Control flow is explicit
- Self-documenting - Error types are visible in the signature
- Composable - Works well with functional patterns