IOException in Java
IOException
is the most generic exception in a large group of Java exceptions that express input/output and networking errors in Java applications.
IOException: quick facts
- Qualified class name:
java.io.IOException
. - Exception type: checked exception.
- Summary: signals that an input/output (I/O) operation has failed or was interrupted.
- Notable related exceptions:
FileNotFoundException
,EOFException
,MalformedInputException
,FileSystemException
,MalformedURLException
,UnknownHostException
,HttpTimeoutException
, and 50+ more exception types directly derived fromIOException
.
When and why does IOException usually occur in Java?
IOException
gets thrown when a Java application encounters an input/output error. This may happen for a variety of reasons, such as:
- The specified file can’t be read or written to because it does not exist, is locked by another process, or is inaccessible due to insufficient permissions.
- A network connection used to write or read data is lost, the remote host becomes unavailable, or a socket has been closed unexpectedly.
- A thread is interrupted while performing an I/O operation.
Since IOException
is a superclass of a variety of more specific I/O-related exceptions in Java, it is not always thrown directly when I/O problems occur. However, it’s very often used in catch
clauses, which allows catching both the IOException
itself and any of its 50+ derived exceptions.
What are some common scenarios when Java APIs throw IOException?
When using various Java APIs available with the JDK, you can commonly see instances of IOException
thrown with the following messages:
java.io.IOException: Connection reset by peer
when one end of a network exchange abruptly closes the connection while the other end is still using it. This may happen if a user has shut down their client application, or if the connection has been inactive longer than a maximum idle time limit. Normally, aIOException
with this message is a warning sign that you need to look into environment configuration.java.io.IOException: Stream closed
when your application tries to consume an input or output stream after it has been explicitly or implicitly closed.java.io.IOException: No space left on device
when your application tries to write an object to disk without checking if the necessary disk space is available, or when a Unix file system is out of inodes.java.io.IOException: Too many open files
when your Java application runs out of file descriptors that the operating system is willing to provide. This is a signal that your application may be opening a lot of files without closing the files opened previously.
Is IOException a checked or unchecked exception?
IOException
is a checked exception, which means that the Java compiler requires you to handle it. Any time you use a method that can potentially throw IOException
, you need to either:
- Handle this exception using a
try/catch
block, or - Add a
throws
clause to your method to delegate the responsibility of handling this exception to callers of your method.
If you don’t do any of this, the Java compiler will prevent you from compiling your application:
../IOExceptionSample.java:67:52
java: unreported exception java.io.IOException; must be caught or declared to be thrown
What Java exceptions are related to IOException?
IOException
is a direct subtype of Exception
, the base type of all checked exceptions in Java. Here’s the class hierarchy for IOException
:
Throwable (java.lang)
Exception (java.lang)
IOException (java.io)
IOException
is the most generic exception in a large group of exceptions that express input/output and networking errors in Java applications. OpenJDK 17 ships with over 100 direct and indirect inheritors of IOException
, making it one of the largest Java exception groups available to developers.
Some of the most widely used subtypes of IOException
are SocketException
, FileNotFoundException
, EOFException
, MalformedInputException
, HttpTimeoutException
, ClosedChannelException
, ZipException
, and RemoteException
.
IOException example: using a BufferedReader
IOException
is a checked exception, and the Java compiler won’t get off your back until you handle it one way or another. For example, what if you try to compile a Java application that contains the following method?
static void readFromFile(String fileName) {
FileReader reader = new FileReader(fileName);
BufferedReader bufferedReader = new BufferedReader(reader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
}
Compilation will fail with an error message:
../IOExceptionSample.java:54:47
java: unreported exception java.io.IOException; must be caught or declared to be thrown
That’s because java.io.BufferedReader.readLine()
in the JDK doesn’t handle I/O errors by itself. Instead, it uses the throws
clause to delegate handling of IOException
to its callers as they see fit:
public String readLine() throws IOException {
return readLine(false, null);
}
What do you do about it? As the error message listed above implies, you need to either catch IOException
or declare it in your method signature with a throws
clause.
How to catch IOException
Let’s see how you can catch the exception from the previous code sample, as well as any exceptions that derive from it. You can do it in the same method where it may occur, or elsewhere up the call stack using the throws
clause.
Handling IOException with the throws clause
Sometimes, you may want to avoid handling an exception inside a method where it’s called, delegating this to a different method in your call stack. To do this, you can add a throws
clause to the method:
static void readFromFile(FileReader reader) throws IOException {
BufferedReader bufferedReader = new BufferedReader(reader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
}
Doing so would make callers of the readFromFile()
method responsible for handling the IOException
. Unless this method is not used, trying to compile again will result in a similar compiler error that relocates to the method’s call sites:
../Main.java:19:40
java: unreported exception java.io.FileNotFoundException; must be caught or declared to be thrown
You could then add a similar throws
clause to the calling method, and continue up the call stack all the way to your application’s main()
method. If the main()
method gets IOException
added to its throws
clause as well, the compiler will give up and let you build the application. However, as soon as the exception actually occurs, your application will simply exit without a chance of recovery. While this is technically possible, leaving your application without error handling code is a bad idea.
However, it’s perfectly valid to use the throws
clause when your application implements a strategy of handling exceptions up the call stack in a centralized manner. If you’re writing a library, use the throws
clause if you believe that its users will benefit from flexibility in handling error conditions more than they will struggle from having to add extra exception handling code.
In Java frameworks such as Spring REST services, you can use the throws
clause to propagate IOException
and other exceptions all the way to the REST controller and handle them there, transforming internal exception details into clear error messages that API consumers can understand.
Handling IOException using the try/catch block
If you want to handle the exception right where the call to readLine()
occurs, you need to place the call into the try
clause of a try/catch
block — for example, like this:
static void readFromFile(String fileName) {
FileReader reader = new FileReader(fileName);
BufferedReader bufferedReader = new BufferedReader(reader);
try {
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException exception) {
// Exception handling code here
}
}
Handling IOException and its subclasses
But wait, if you run the compiler with the code above, it will fail again, this time with the following message:
../IOExceptionSample.java:51:29
java: unreported exception java.io.FileNotFoundException; must be caught or declared to be thrown
That’s because the method also creates a FileReader
instance, and the FileReader
constructor throws another checked exception, FileNotFoundException
. How can you take care of it?
One way would be to surround the creation of a FileReader
instance with a separate try/catch
block, like this:
static void readFromFile(String fileName) {
FileReader reader;
try {
reader = new FileReader(fileName);
} catch (FileNotFoundException e) {
// FileNotFoundException handling code goes here
}
BufferedReader bufferedReader = new BufferedReader(reader);
try {
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException exception) {
// IOException handling code goes here
}
}
While this works, the share of exception handling code in the body of this method is getting way out of control.
What you could do instead is simply move the first two lines of the method into the same try
block that wraps the readLine()
call:
static void readFromFile(String fileName) {
try {
FileReader reader = new FileReader(fileName);
BufferedReader bufferedReader = new BufferedReader(reader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (FileNotFoundException exception) {
// FileNotFoundException handling code here
} catch (IOException exception) {
// IOException handling code here
}
}
This is better, and if you want to handle FileNotFoundException
and IOException
separately, you may want to stick with this version.
However, if you’re just fine handling these two exceptions in a similar way, you can make the method even shorter:
static void readFromFile(String fileName) {
try {
FileReader reader = new FileReader(fileName);
BufferedReader bufferedReader = new BufferedReader(reader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException exception) {
// Exception handling code here
}
}
Note that FileNotFoundException
is no longer caught explicitly. This is possible because IOException
is a superclass of FileNotFoundException
. The catch
clause that targets IOException
lets you catch both IOException
itself and all of its exception subclasses, including FileNotFoundException
.
Handling IOException in different ways
How exactly you handle IOException
will vary. In a console application, it may be enough to simply print the exception’s stack trace:
catch (IOException exception) {
e.printStackTrace();
}
If you’re logging errors instead of relying on console output, you can log the exception and specify relevant details, such as the name of the file that your application failed to read:
try {
LOGGER.addHandler(new FileHandler("logger.log"));
...
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException exception) {
LOGGER.log(Level.SEVERE, MessageFormat.format("Failed to read from file {0}", fileName), exception);
}
You may also want to rethrow the exception as a new unchecked exception – that is, RuntimeException
:
catch (IOException ex) {
throw new RuntimeException(ex);
}
Rethrowing as an unchecked exception is an alternative to using the throws
clause that also allows to delegate handling the exception to a different method up the call stack.
When you’re declaring an exception using the throws
clause, every method up the call stack also needs to declare that exception unless it handles it. As a result, if you have a long chain of method calls, you end up having to declare the exception in each of them:
static void throwsVsRethrowAsRuntime() {
method1WithThrows("file.txt");
}
static void method1WithThrows(String fileName) {
try {
method2WithThrows(fileName);
} catch (IOException e) {
// Actually handle the exception
}
}
static void method2WithThrows(String fileName) throws IOException {
method3WithThrows(fileName);
}
static void method3WithThrows(String fileName) throws IOException {
method4WithThrows(fileName);
}
static void method4WithThrows(String fileName) throws IOException {
readFromFileThrows(fileName);
}
static void readFromFileThrows(String fileName) throws IOException {
FileReader reader = new FileReader(fileName);
BufferedReader bufferedReader = new BufferedReader(reader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
}
In contrast, when you’re rethrowing IOException
(or any checked exception in general) as RuntimeException
, you can still catch it somewhere up the call stack, but you don’t need to modify declarations of all methods in the call chain:
static void throwsVsRethrowAsRuntime() {
method1Rethrowing("file.txt");
}
static void method1Rethrowing(String fileName) {
try {
method2Rethrowing(fileName);
} catch (Exception exception) {
// Actually handle the exception
}
}
static void method2Rethrowing(String fileName) {
method3Rethrowing(fileName);
}
static void method3Rethrowing(String fileName) {
method4Rethrowing(fileName);
}
static void method4Rethrowing(String fileName) {
readFromFileRethrowAsRuntime(fileName);
}
static void readFromFileRethrowAsRuntime(String fileName) {
try {
FileReader reader = new FileReader(fileName);
BufferedReader bufferedReader = new BufferedReader(reader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Whether to use the throws
clause or rethrow checked exceptions as RuntimeException
is up for debate. Go with your personal preference, or, if you’re working on a team, with whatever your team coding guidelines define.
Production debugging isn’t scary with Lightrun
Properly handling exceptions is one thing that you as a developer can do to ensure a smooth ride in production for your Java applications. Still, let’s face it: any non-trivial application will have bugs. You’re lucky if you can reproduce a bug in a local environment, debug and happily push a verified fix.
What if you can’t? Debugging remotely is tricky: you need to rely on existing logging, repeatedly redeploy updates with more logs and attempted fixes, and you’re even unable to set a proper breakpoint because you can’t afford to halt a production environment.
Take a look at Lightrun: our next-gen remote debugger for your production environment. With Lightrun, you can inject logs without changing code or redeploying, and add snapshots: breakpoints that don’t stop your production application. Lightrun supports Java, .NET, Python and Node.js applications, integrates with IntelliJ IDEA and VS Code. Set up a Lightrun account and check for yourself!
It’s Really not that Complicated.
You can actually understand what’s going on inside your live applications.