How to properly handle an IOException from close()

2019-03-15 02:12发布

问题:

The Java I/O classes java.io.Reader, java.io.Writer, java.io.InputStream, java.io.OutpuStream and their various subclasses all have a close() method that can throw an IOException.

Is there any consensus on the proper way to handle such exceptions?

I have often seen recommendations to just silently ignore them, but that feels wrong, and at least in case of resources opened for writing, a problem while closing the file might mean that unflushed data could not be written/sent.

On the other hand, when reading resources, I'm totally unclear on why close() might throw and what to do about it.

So is there any standard recommendation?

A related question is Does close ever throw an IOException?, but that is more about which implementations really do throw, not about how to handle the exceptions.

回答1:

Log it.

You can't really do anything about it (for example write some code that recovers from the error), but its generally worth letting somebody know about it.

Edit:
After further investigation and reading the other comments, I'd say that if you do want to handle it then you're going to have to know details of the implementation. Conversely you probably need to know details of the implementation to decide whether you need to handle it.

Realistically though, I can't think of any examples of streams where the reading or writing would work correctly without throwing an exception, but the closing would.



回答2:

"Ignoring or just logging is usually a bad idea." That's not answering the question. What should one do about an IOException from close()? Just rethrowing it only pushes the problem further up, where it's even more difficult to handle.

Theory

To answer you question directly: When this IO action failed, then depending on the circumstances, you may want to

  • rollback changes (don't leave a partially written file on the disk, delete it)
  • retry (maybe after a rollback)
  • ignore (and continue)
  • interrupt the current task (cancel)

You can often see the last 3 in dialog boxes shown to the user. Indeed, delegating choice to the user is a possibility.

I believe the main point is to not leave the system in an inconsistent state. Just swallowing a close exception might leave you with a crippled file, causing nasty errors later on.

Practice

It is a bit cumbersome to work with checked exceptions. Options arise:

  • Convert them to RuntimeException, throw them and leave them to be handled at a sufficiently high level

  • Keep using checked exceptions and suffer the syntactic pain

  • Use more composable error handling constructs, like the IO monad (not really available in Java, at least not without embarassingly many braces (see http://apocalisp.wordpress.com/2008/05/16/thrower-functor/) and not with full power)

More on IO monad

When you return a result in the IO monad context, you are not actually executing an IO action, but rather return the description of that action. Why? These actions have an API with which they compose nicely (versus the throws/try/catch massacre).

You can decide if you wish to do check exception style handling (with Option or Validation˙ in the return type), or dynamic style handling, with bracketing only on the finalunsafePerformIO` call (which actually executes the composed IO action).

To get some idea, see my Scala gist http://gist.github.com/2709535, which hosts the executeAndClose method. It returns both the result of the resource processing (if available) and an unclosed resource (if closing failed). It is then the users decision how to deal with such an unclosable resource.

It could be beefed up with Validation/ValidationNEL instead of Option if one needs the one/more actual exceptions too.



回答3:

Your code that calls InputStream.close() or OutputStream.close() is some high level code that delegates to the lower level InputStream or OutputStream class to perform some high level computation.

Whether to throw an exception

You have only 3 choices about what to do.

  • Catch and swallow the exception, discarding it.
  • Let it propagate upwards.
  • Catch the exception, and throw a different kind of exception. You would normally use a chained exception in this case.

The last two options are similar in that your code throws an exception. So this question is actually a special case of the question of when should my code throw an exception. The correct answer to that question in this context is: if, and only if, the alternative, is to fail to meet a post condition of your code or to maintain an invariant of your code. The post conditions specify what your method is meant to do. The invariants specify characteristics of the class.

So, you need to ask yourself whether the close() throwing an exception prevents your method doing what is should do.

  • If it does not prevent your method doing its job, the correct thing to do is to swallow the exception. Despite what many people will tell you.
  • If close() throwing does prevent your method doing its job, and the method may throw IOException, you can do nothing, just letting the exception propagate upwards.
  • If close() prevents your method doing its job, but the method may not throw IOException, you must catch the IOException and rethrow it as a different class of exception, recording the IOException as the cause of the thrown exception.

I know of no circumstances in which a InputStream.close() throwing an exception can prevent a computation succeeding. That call to close() naturally happens after you have finished reading whatever data you are interested in.

Output streams, however, can buffer data to be written to the output destination, either internally (within the Java code) or at a lower level (within the operating system). You therefore can not be sure that write operations to an output stream have actually resulted in real output until OutputStream.close() has successfully returned (without throwing an exception). You should therefore treat an exception thrown by OutputStream.close() just like a write failure.

Your method is responsible for maintaining its invariants. Sometimes this will require a clean-up or roll-back operation if close() throws an exception. You should put the code for that in a catch or finally clause for the IOException, even if you would like the exception to propagate upwards. If you use a catch clause and you want to propagate it, you will have to rethrow it.

Whether to log the exception

Some people will advise you to log the exception. This is almost always bad advice. Log messages are part of the user interface of your program. Fundamentally, you must always ask yourself whether to log anything at all, because useless verbiage can distract and confuse the users reading your log file ("users" includes system administrators). Every message logged should be for some useful purpose. Each should provide information that helps the user make decisions.

Reporting specifically that close() failed is rarely useful. How can it help a user make a decision? If the exception did not prevent your method from doing its job, there is no problem, and no actions by the user are necessary. If your program failed to close() the stream, and that is a problem, what can the user do to help?

Low level code is not usually responsible for logging at all. It instead performs abstract operations, reporting failure to higher level parts of your program by throwing exceptions. Code closing a stream is typically rather low level, so the code that detects that close() is too low level to do any logging.

The specific fact that close() failed is rarely useful. What can be useful is knowing that the abstract operation that your method is meant to perform failed. That can be done by having the higher level code catch all expected exceptions and reporting that the operation failed, rather than having your method precisely report that "close failed".



回答4:

Try to use flush before you close the writer. The main reason for exception in close is that some other resources might have been using the data or the writer/reader may not be open at all. try to find wheather the resoures is open before closing it.



回答5:

First of all don't forget to put the close() inside the "finally" section of your try catch block. Second you can surround the close method with another try catch exception and log the exception.



回答6:

It depends on what you are closing. For example, closing a StringWriter does nothing. The javadocs are clear on that. In this case, you can ignore the IOException because you know that it will never be generated. Technically, you don't even need to call close.

/**
 * Closing a <tt>StringWriter</tt> has no effect. The methods in this
 * class can be called after the stream has been closed without generating
 * an <tt>IOException</tt>.
 */
public void close() throws IOException {
}

For other streams, log and handle the exception as appropriate.



回答7:

Generally resource handling should look like this:

final Resource resource = context.acquire();
try {
    use(resource);
} finally {
    resource.release();
}

In (the unlikely) case of release throwing an exception, any exception thrown by use will be dropped. I don't have a problem with the exception thrown closest to the catcher winning. I believe ARM blocks in JDK7(?) will do something crazy in this case, like rethrowing the use exception with the release exception attached.

If you use the Execute Around idiom, you can put these decisions and potentially messy code in one (or few) places.



回答8:

Ignoring or just logging is usually a bad idea. Even though it is true that you cannot recover from it, I/O exceptions should always be handled in a unified manner to make sure your software behaves in a unified way.

I would at least suggest handling like below:

//store the first exception caught
IOException ioe = null;
Closeable resource = null;
try{
  resource = initializeResource();
  //perform I/O on resource here
}catch(IOException e){
  ioe = e;
}finally{
  if (resource != null){
    try{
      resource.close();
    }catch(IOException e){
      if(null == ioe){
        //this is the first exception
        ioe = e;
      }else{
        //There was already another exception, just log this one.
        log("There was another IOException during close. Details: ", e);
      }
    }
  }
}

if(null != ioe){
  //re-throw the exception to the caller
  throw ioe;
}

The above is pretty verbose but works well, because it will prefer the IOException during I/O on the resource to the one during close, since it is more likely to contain information of interest to the developer. The code will also throw an IOException when something goes wrong, so you get a unified behaviour.

There might be nicer ways of doing it, but you get the idea.

Ideally, you would create a new exception type that would allow storing sibling exceptions, so in case you get two IOExceptions you could store them both.



回答9:

Well, in most cases, close() doesn't actually throw an IOException. Here's the code for InputStream.java:

  public void close() throws IOException
  {
    // Do nothing
  }

Errors from closing a network resource should really be of some type of RuntimeException, since you can disconnect a networked resource after the program connects to it.

You can see some example of various implementations of Reader/Writer and Streams using Google Code Search. Neither BufferedReader nor PipedReader actually throw an IOException, so I think you're mostly in the safe by not worrying about it. If you're really worried, you can check the implementation of the libraries you're using to see if you ever need to worry about the exception.

Like others mentioned, you can't do much about the IOException other than log it.

After all, try/catch blocks in the finally clause are pretty ugly.

Edit:

Further inspection reveals subclasses of IOException like InterruptedIOException, SyncFailedException, and ObjectStreamException, along with classes that inherit from it. So just catching an IOException would be too general -- you wouldn't know what to do with the information other than logging it because it could be from a whole range of errors.

Edit 2:

Urk, BufferedReader was a bad example, since it takes a Reader as input. I've changed it to InputStream.java

However, there's a hierarchy with InputStream <= FilterInputStream <= BufferedInputStream <= InputStreamReader (via inheritance and private instances) that all trickle up to the close() method in InputStream.