Java exceptions are great for truly exceptional conditions (things like I/O failures), but using them for expected outcomes — “user not found”, “invalid password”, “email already taken” — is expensive and semantically wrong.
Since Java 17+, sealed classes + records + pattern matching give us something very close to Rust’s Result<T, E> or Kotlin’s sealed classes — with zero runtime overhead and full compile-time exhaustiveness checking.
Here’s how to do it properly in modern Java.
The old way: Please stop using it!
public User getUser(String id) throws UserNotFoundException { … }
try {
User user = service.getUser(id);
} catch (UserNotFoundException expectedButStillInCatch) { … }
The modern way:
First of all, we can create a reusable interface which emulates Rust’s great Result type:
public sealed interface Result<S, F>
permits Result.Ok, Result.Err {
record Ok<S, F>(S value) implements Result<S, F> {}
record Err<S, F>(F error) implements Result<S, F> {}
// Factory methods
static <S, F> Result<S, F> ok(S value) { return new Ok<>(value); }
static <S, F> Result<S, F> err(F error) { return new Err<>(error); }
// Convenience
default boolean isOk() { return this instanceof Ok; }
default boolean isErr() { return this instanceof Err; }
default S unwrap() { return ((Ok<S, F>) this).value(); }
default F unwrapErr() { return ((Err<S, F>) this).error(); }
}
Next we can create the domain error type:
public sealed interface UserError permits
UserError.NotFound,
UserError.AlreadyExists {
record NotFound(UserId id) implements UserError {}
record AlreadyExists(Email email) implements UserError {}
}
And then we can create the domain objects:
public record UserId(String id) {}
public record Email() {}
public record CreateUserRequest(Email email) {}
public record User(UserId id, Email email) {}
And here’s a a sample service that uses it all.
@Service
public class UserService {
public Result<User, UserError> getUserById(UserId id) {
return userRepository.findById(id)
.map(Result::<User, UserError>ok)
.orElse(Result.err(new UserError.NotFound(id)));
}
public Result<User, UserError> createUser(CreateUserRequest req) {
if (userRepository.existsByEmail(req.email())) {
return Result.err(new UserError.AlreadyExists(req.email()));
}
User user = userRepository.save(req.toUser());
return Result.ok(user);
}
}

Leave a comment