skip to content

Compare the two completion styles of async channels: Future-based versus CompletionHandler. When would you choose each?

level: middleimportance: should knowfreq 45%

answer

  1. Future: poll isDone / await get
  2. Handler: completed() + failed() callbacks
  3. get() re-introduces blocking by choice
  4. attachment carries context to the callback
  5. Don't do heavy work on pool threads

basics

~20 s

With the Future style you start an operation and later poll or wait on a Future for the byte count. With the CompletionHandler style you pass a callback object whose completed() or failed() method runs automatically when the operation ends, so you never wait.

solid answer

~50 s

Both retrieve the outcome of an async operation but differ in control flow. The Future overload returns a Future<Integer>; you call isDone() to poll or get() to await the byte count, with get() blocking the caller — useful when you want fire-and-then-collect or to run a few operations and join them. The CompletionHandler overload takes a handler and an attachment object; the channel group invokes completed(result, attachment) on success or failed(throwable, attachment) on error, on a pool thread, so the initiating thread stays free. Choose Future for simple, occasional async needs or when you genuinely want to block later. Choose CompletionHandler for event-driven servers handling many concurrent operations where blocking any thread defeats the purpose. A pitfall of the handler style is callback chaining (initiating the next read inside completed), which can grow deep and must avoid heavy work on pool threads.

go deeper

for a junior

Knows there are two ways to get the result: a Future you check, or a callback that runs when done.

for a middle

Correctly states that Future.get() blocks while CompletionHandler does not, and names completed/failed and the attachment.

for a senior

Picks the right style per use case and explains pool-thread hazards (heavy work, get() deadlocks).

for a principal

Frames the choice as inversion-of-control trade-offs and reasons about back-pressure and thread-pool sizing across the whole system.

### Setup An **asynchronous channel** (e.g. `AsynchronousFileChannel`, `AsynchronousSocketChannel`) starts an I/O operation that finishes later. The two *completion styles* are simply the two ways the API hands the result back to you. Each I/O method (read/write/accept/connect) is overloaded once per style. ### Style 1 — Future-based A **Future<V>** is a placeholder for a value that will exist later. The async method returns `Future<Integer>` where the Integer is the number of bytes transferred. ``` Future<Integer> f = channel.read(buffer, position); // ... do other work ... Integer bytesRead = f.get(); // BLOCKS here until done ``` - `f.isDone()` — non-blocking check: has it finished? - `f.get()` — *blocks* the calling thread until the result is ready, then returns it (or throws `ExecutionException` wrapping the I/O error). - `f.cancel(...)` — attempt to cancel. So Future is 'start now, collect later'. It is convenient but if you call `get()` you have re-introduced blocking — you just chose *when* to block. ### Style 2 — CompletionHandler-based A **CompletionHandler<V, A>** is an interface with two methods you implement: - `completed(V result, A attachment)` — called on success. - `failed(Throwable exc, A attachment)` — called on failure. The `attachment` is any context object you pass in so the callback knows what it is working on (e.g. the buffer or a connection state). The async method has signature like `read(buffer, position, attachment, handler)` and returns `void` — it never gives you a Future. When the I/O finishes, a thread from the **AsynchronousChannelGroup** pool invokes your handler. ``` channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() { public void completed(Integer bytes, ByteBuffer buf) { /* result ready */ } public void failed(Throwable exc, ByteBuffer buf) { /* handle error */ } }); ``` No thread blocks. This is the fully event-driven model. ### Choosing between them - **Future** — simpler mental model; good when you start a small number of operations and are happy to block to join them, or for occasional async work mixed into otherwise sequential code. - **CompletionHandler** — required for true scalability: thousands of in-flight operations, none of which should tie up a thread. The cost is *inversion of control* (your logic lives in callbacks) and the risk of deep callback chains and of doing slow work on the limited pool threads, which starves other operations. ### Gotchas - Doing CPU-heavy work inside `completed()` runs it on a channel-group pool thread and can stall other completions — offload to a separate executor. - Mixing `Future.get()` calls on a thread that is also a channel-group thread can deadlock if the pool is small. - With CompletionHandler you must remember `failed()` — silent error-swallowing is a common bug.

  • What is the attachment parameter for in the CompletionHandler API?
    It is an arbitrary context object passed at submit time and handed back to completed()/failed(), letting a single shared handler know which buffer, connection, or state object the completed operation belongs to without per-operation closures.
  • Can calling future.get() on a channel-group thread cause problems?
    Yes. If the pool is small and the thread that must run the completion is the same one blocked in get(), you can deadlock. Prefer the CompletionHandler style, or ensure get() is called from outside the group.

saying these in an interview costs you the question

  • Saying Future is non-blocking — get() blocks; only isDone() is non-blocking
  • Forgetting the failed() method exists, swallowing errors
  • Running expensive computation inside completed() on a channel-group thread
  • Claiming you can mix both styles on one operation — each call uses exactly one

context