Why are block(), blockFirst(), and blockLast() forbidden on event-loop threads, and where can you legitimately call them?
answer
- block = subscribe + park via latch
- Netty threads implement NonBlocking marker
- ISE: 'blocking ... not supported in thread reactor-http-nio-N'
- OK in tests / main() / CommandLineRunner
- In handlers: return the Mono, don't block it
basics
~10 sblock() waits (parks the thread) until the reactive stream finishes. On an event-loop thread that freezes request processing, so Reactor throws an error. It's fine in tests, main(), or plain non-reactive threads.
solid answer
~40 s`block()`/`blockFirst()`/`blockLast()` subscribe to a `Mono`/`Flux` and then park the calling thread until a value (or completion/error) arrives — they bridge reactive code back into imperative style. On a Netty event-loop thread (`reactor-http-nio-*`) or a `Schedulers.parallel()` thread, parking is illegal: Reactor marks these as non-blocking-only and throws `IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-N`. It defeats the entire point of reactive I/O and starves the loop. Legitimate places to call block: `main()` methods, `@Test` methods (e.g. verifying a `Mono`), CommandLineRunner startup code, or any ordinary non-reactive thread where you're intentionally leaving reactive-land. Inside a controller/handler you should return the `Mono`/`Flux` and let the framework subscribe — never block it yourself.
code
java · 19 lines// Reactor's guardrail: this throws at runtime on the event loop
// java.lang.IllegalStateException: block()/blockFirst()/blockLast()
// are blocking, which is not supported in thread reactor-http-nio-2
@GetMapping("/name/{id}")
public String wrong(@PathVariable Long id) {
return userService.findName(id).block(); // DON'T
}
// Correct: hand the Mono to the framework
@GetMapping("/name/{id}")
public Mono<String> right(@PathVariable Long id) {
return userService.findName(id);
}
// Legitimate block() in a test (ordinary test thread):
@Test
void findsName() {
assertEquals("ada", userService.findName(1L).block());
}go deeper
Knows block() waits and shouldn't be used in a WebFlux controller; return the Mono instead.
Explains the NonBlocking marker + the exact IllegalStateException, and lists legit call sites (tests, main).
Also catches the disguised forms: toFuture().get() and blocking DAO calls inside operator lambdas.
Enforces 'return the publisher' as a code-review rule and knows boundedElastic threads are intentionally blockable.
**What these operators do.** `Mono.block()`, `Flux.blockFirst()`, and `Flux.blockLast()` are the *bridge out* of reactive programming. They **subscribe** to the publisher and then **block the current thread** (using an internal `CountDownLatch`) until the stream emits, completes, or errors — then return the value (or throw). They convert an asynchronous `Mono<T>`/`Flux<T>` into a synchronous `T`. **Why they're forbidden on the loop.** Reactor tags certain threads as **non-blocking only** by having them implement the marker interface `reactor.core.scheduler.NonBlocking` (Netty event-loop threads and `Schedulers.parallel()` workers do this). When you call `block()` on such a thread, Reactor checks the marker and immediately throws: `java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-2` This is a *built-in guardrail* — Reactor refuses because parking an event-loop thread would stop it from servicing all other connections. Note this check is Reactor's own; it fires even without BlockHound. **The subtle trap.** People reach for `block()` to 'just get the value' inside a handler — e.g. `String name = userService.findName(id).block();`. Under real traffic that either throws the ISE above or, on a thread not marked non-blocking, silently starves throughput. The correct pattern is to **compose**: `return userService.findName(id).map(...)` and hand the `Mono` back to the framework, which subscribes on the right scheduler. **Where blocking is legitimate.** - **Tests:** `StepVerifier` is preferred, but `mono.block()` in a JUnit test is common and fine — the test thread is an ordinary thread. - **`main()` / bootstrap:** e.g. a `CommandLineRunner` or a CLI that must wait for a result before exiting. - **Truly non-reactive worker threads** you control (e.g. code offloaded onto `boundedElastic`, which is *not* marked non-blocking) — though even there, prefer composition. **Related gotchas.** - `toFuture()` + `future.get()` is the same trap in disguise — `get()` blocks. - Blocking inside a `map`/`flatMap` lambda (calling a blocking DAO) blocks the thread the operator runs on, which by default is the subscribing (event-loop) thread — so it's just as bad as `block()`. - `Schedulers.boundedElastic()` threads are *allowed* to block (not marked `NonBlocking`), which is exactly why you offload blocking work there. **Rule of thumb:** in production reactive code, if you typed `.block()`, you probably made a mistake — return the publisher instead.
- How does Reactor know a thread is 'non-blocking only'?Those threads (Netty event-loop, Schedulers.parallel()) implement Reactor's NonBlocking marker interface. block()/blockFirst()/blockLast() check for it and throw IllegalStateException. boundedElastic threads don't implement it, so blocking is allowed there.
- Is calling a blocking DAO inside a flatMap lambda safe if you don't call block()?No. The lambda runs on whatever thread the chain is subscribed on — by default the event loop — so a blocking DAO call there parks it just like block() would. You still need subscribeOn(boundedElastic()).
saying these in an interview costs you the question
- Calling .block() in a controller to 'unwrap' the value
- Thinking block() throwing is a bug rather than a deliberate guardrail
- Believing avoiding block() but calling blocking code inside map/flatMap is safe