1. Error handling in asynchronous operations
In synchronous code it’s simple: if a file is not found or access is denied, you immediately catch the exception in a try-catch. In asynchronous code, especially when you use callbacks (CompletionHandler), an error can occur after your method has already returned — somewhere deep in the thread pool. If you don’t handle it properly, the program may behave unpredictably: from “silent” data loss to crashing the entire application.
How are errors delivered to CompletionHandler?
The CompletionHandler<V, A> interface has two methods:
- completed(V result, A attachment) — called if the operation completed successfully.
- failed(Throwable exc, A attachment) — called if an error occurred.
Here’s an example:
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;
public class AsyncErrorDemo {
public static void main(String[] args) throws Exception {
Path path = Paths.get("nonexistent.txt");
ByteBuffer buffer = ByteBuffer.allocate(1024);
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("Successfully read " + result + " bytes");
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("File read error: " + exc.getMessage());
// You can log, notify the user, or propagate the error further
}
});
} catch (IOException ex) {
System.out.println("File open error: " + ex.getMessage());
}
// Give the async operation time to finish (in real applications use CountDownLatch or other mechanisms)
Thread.sleep(500);
}
}
What happens here?
- If the file does not exist, the failed method will be called with the corresponding exception (NoSuchFileException).
- If the operation completed successfully — completed will be invoked.
Examples of common errors
- File not found: NoSuchFileException
- Access denied: AccessDeniedException
- Read/write error: various subclasses of IOException
- Buffer issues: BufferOverflowException, BufferUnderflowException
Logging and informing the user
An error in an async callback is not a reason to panic, but it’s also not a reason to pretend nothing happened. A good practice is to log the error (for example, via Logger), and if it matters to the user — show a message or invoke a handler in the UI.
Logging example:
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.err.println("Asynchronous operation error: " + exc);
exc.printStackTrace();
}
In production code use proper loggers (for example, java.util.logging or Log4j), not System.err.
2. Canceling asynchronous operations
When might you need to cancel an operation?
Sometimes an asynchronous task needs to be stopped right in the middle of its work. For example, the user changed their mind and clicked “Cancel” during a file download. Or the application window was closed and the operation no longer makes sense. And when shutting down the application, you may simply need to cleanly release resources.
For such cases, asynchronous I/O in Java supports cancelation through the Future interface. With it you can interrupt a running task at any moment and avoid wasting resources.
How to cancel an operation with Future?
The read or write method of AsynchronousFileChannel returns a Future<Integer> object. This object has a cancel(boolean mayInterruptIfRunning) method.
Example: canceling an asynchronous read
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;
public class AsyncCancelDemo {
public static void main(String[] args) throws Exception {
Path path = Paths.get("bigfile.txt");
ByteBuffer buffer = ByteBuffer.allocate(1024);
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
Future<Integer> future = channel.read(buffer, 0);
// Wait a bit, then cancel the operation
Thread.sleep(100);
boolean cancelled = future.cancel(true);
if (cancelled) {
System.out.println("Read operation canceled!");
} else {
System.out.println("Failed to cancel the operation (it may have already finished)");
}
}
}
}
Important nuances:
- Cancelation works only for operations that have not yet completed.
- If the operation has already finished — you won’t be able to cancel it.
- After cancelation, attempting to call get() on that Future will throw a CancellationException.
When is it too late to cancel an operation?
If the task has already finished — whether successfully or with an error — you can no longer stop it; the ship has sailed.
Moreover, not all implementations can actually interrupt operations at the operating system level. For example, with some file systems “cancel” will be purely symbolic: the operation will continue, but you will simply ignore the result.
3. Practice: error handling and cancellation
Example 1: handling an error when reading a nonexistent file
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.io.IOException;
public class AsyncErrorExample {
public static void main(String[] args) throws Exception {
Path path = Paths.get("no_such_file.txt");
ByteBuffer buffer = ByteBuffer.allocate(1024);
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("The operation completed successfully");
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("Error reading file: " + exc.getClass().getSimpleName() + " - " + exc.getMessage());
}
});
} catch (IOException ex) {
System.out.println("Error opening file: " + ex.getMessage());
}
Thread.sleep(500);
}
}
What will we see in the console?
Error opening file: no_such_file.txt
or, if the error occurs during reading rather than opening:
Error reading file: NoSuchFileException - no_such_file.txt
Example 2: canceling a long operation and shutting down cleanly
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;
public class AsyncCancelExample {
public static void main(String[] args) throws Exception {
Path path = Paths.get("bigfile.txt");
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 10 MB
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
Future<Integer> future = channel.read(buffer, 0);
// After 50 ms, cancel the operation (for the experiment)
Thread.sleep(50);
boolean cancelled = future.cancel(true);
if (cancelled) {
System.out.println("The read operation was canceled!");
} else {
System.out.println("Failed to cancel the operation (likely already finished)");
}
try {
// Try to get the result (will throw CancellationException)
future.get();
} catch (java.util.concurrent.CancellationException ex) {
System.out.println("Caught CancellationException: the operation is indeed canceled.");
}
}
}
}
4. Best practices: how to do it right
Release resources even on errors
Use try-with-resources for automatic channel closing:
try (AsynchronousFileChannel channel = /* open channel */ null) {
// ...
}
If you use CompletionHandler, don’t forget to close the channel when all operations finish. This is especially important if you perform several asynchronous operations in a row.
Do not block the UI/main thread
Asynchronous operations are meant not to block the main thread. Do not call future.get() on the UI thread — otherwise the point of asynchrony is lost.
Log all errors
In CompletionHandler, always implement the failed method and log (or propagate) all exceptions.
Ensure all operations finish before the program exits
If the program exits before an operation completes, the result may be lost. For console demos, it’s sometimes acceptable to do Thread.sleep(500), but in real applications use CountDownLatch, CompletableFuture, or other synchronization mechanisms.
Don’t forget about cancellation
If an operation is no longer needed (for example, the user closed the window), cancel it via Future.cancel. This will save resources and improve application responsiveness.
5. Common mistakes in error handling and cancellation in async I/O
Error #1: Ignoring the failed method in CompletionHandler.
If you don’t implement error handling, your application will behave unpredictably: errors will be “lost,” and the user won’t know why nothing is happening.
Error #2: The channel is not closed after operations finish.
If you forget to close AsynchronousFileChannel, you’ll get resource leaks and possibly an OS-level file lock.
Error #3: Waiting for an async operation’s result on the main thread.
You called future.get() on the UI thread — the interface “froze,” and the whole point of asynchrony is gone.
Error #4: Trying to cancel an operation that has already completed.
You called cancel() too late — the operation has already finished, so cancellation won’t work. It’s not critical, but it can be confusing during debugging.
Error #5: Not checking the cancellation result.
You called cancel() but didn’t check the return value and didn’t handle CancellationException when calling get() — the program may crash or behave strangely.
Error #6: Not releasing resources on error or cancellation.
If you don’t close the channel after an error or cancellation, you may run into leaks or file locks.
GO TO FULL VERSION