IOException in Java
  • 02-May-2023
Jura Gorohovsky
Author Jura Gorohovsky
Share
IOException in Java

IOException in Java

Jura Gorohovsky
Jura Gorohovsky
02-May-2023

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 from IOException.

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, a IOException 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!

Share

It’s Really not that Complicated.

You can actually understand what’s going on inside your live applications.

Try Lightrun’s Playground

Lets Talk!

Looking for more information about Lightrun and debugging?
We’d love to hear from you!
Drop us a line and we’ll get back to you shortly.

By submitting this form, I agree to Lightrun’s Privacy Policy and Terms of Use.