Handling Empty Mono Values and Conditional Logic in Project Reactor
Imperative baseline
public void imperativeCheck(Token token) {
if (token == null) {
// business logic for missing token
return;
}
if (token.isExpired()) {
// business logic for expired token
return;
}
// business logic for valid token
}
A naive reactive rewrite that fails
public Mono<Void> naiveReactiveCheck(Mono<Token> token$) {
return token$
.flatMap(t -> {
if (t == null) {
// Unreachable: flatMap is not invoked when the source is empty
// business logic for missing token
return Mono.empty();
}
if (t.isExpired()) {
// business logic for expired token
return Mono.empty();
}
// business logic for valid token
return Mono.empty();
});
}
flatMap (and most operators) act only on onNext signals. If the upstrema Mono compeltes with out emitting, the flatMap lambda is never called. As a result, t is never null inside the lambda; it either exists or the lambda is skipped entirely.
Propagating emptiness with Opitonal
Transform the stream to carry an Optional<Token>, provide a default when the upstream is empty, and then handle all branches in one place.
public Mono<Void> robustReactiveCheck(Mono<Token> token$) {
return token$
// Wrap value-bearing monos as Optional.of(value)
.map(Optional::of)
// If upstream is empty, substitute Optional.empty()
.defaultIfEmpty(Optional.empty())
// Now this stage always runs exactly once
.flatMap(opt -> {
if (opt.isEmpty()) {
// business logic for missing token
return Mono.empty();
}
Token t = opt.get();
if (t.isExpired()) {
// business logic for expired token
return Mono.empty();
}
// business logic for valid token
return Mono.empty();
});
}
Alternatives to empty Mono handling
- Using switchIfEmpty to branch when the source is empty:
public Mono<Void> checkWithSwitch(Mono<Token> token$) {
return token$
.flatMap(t -> {
if (t.isExpired()) {
// business logic for expired token
return Mono.empty();
}
// business logic for valid token
return Mono.empty();
})
.switchIfEmpty(Mono.defer(() -> {
// business logic for missing token
return Mono.empty();
}));
}
- Using defaultIfEmpty with a sentinel and a discriminator:
public Mono<Void> checkWithSentinel(Mono<Token> token$) {
Token SENTINEL = new Token(/* mark as special */);
return token$
.defaultIfEmpty(SENTINEL)
.flatMap(t -> {
if (t == SENTINEL) {
// business logic for missing token
return Mono.empty();
}
if (t.isExpired()) {
// business logic for expired token
return Mono.empty();
}
// business logic for valid token
return Mono.empty();
});
}
- Repeating until a value appears (Flux or Mono with repeatWhenEmpty):
public Mono<Token> fetchWithRetries(Mono<Token> source) {
return source
.repeatWhenEmpty(repeat -> repeat
.take(3) // retry up to 3 times when empty
);
}
Reactor provides defaultIfEmpty, switchIfEmpty, and repeatWhenEmpty to model empty-source behavior without relying on null checks inside flatMap.