javaexceptionioexception

Java method declared to throw IOException not propagating the exception up the call stack


Here is the method:

(388) protected String getExtension(BufferedInputStream stream) throws IOException, MimeTypeException {
(389)   TikaConfig config = TikaConfig.getDefaultConfig();
(390)   MediaType mediaType = config.getMimeRepository().detect(stream, new Metadata());
(391)   MimeType mimeType = config.getMimeRepository().forName(mediaType.toString());
(392)   String extension = mimeType.getExtension(); 
(393)   return extension;
(394) }

And here is the stack:

java.io.IOException: The process cannot access the file because another process has locked a portion of the file
The process cannot access the file because another process has locked a portion of the file
    at java.base/java.io.FileInputStream.readBytes(Native Method)
    at java.base/java.io.FileInputStream.read(FileInputStream.java:328)
    at java.base/java.io.BufferedInputStream.fill(BufferedInputStream.java:291)
    at java.base/java.io.BufferedInputStream.read1(BufferedInputStream.java:347)
    at java.base/java.io.BufferedInputStream.implRead(BufferedInputStream.java:420)
    at java.base/java.io.BufferedInputStream.read(BufferedInputStream.java:399)
    at java.base/java.io.FilterInputStream.read(FilterInputStream.java:95)
    at org.apache.tika.mime.MimeTypes.readMagicHeader(MimeTypes.java:325)
    at org.apache.tika.mime.MimeTypes.detect(MimeTypes.java:527)
    at com.mbm.mediaorganizer.RelocateFileVisitor.getExtension(RelocateFileVisitor.java:390)
    at com.mbm.mediaorganizer.RelocateFileVisitor.relocateFile(RelocateFileVisitor.java:163)
    at com.mbm.mediaorganizer.RelocateFileVisitor.visitFile(RelocateFileVisitor.java:79)
    at com.mbm.mediaorganizer.RelocateFileVisitor.visitFile(RelocateFileVisitor.java:1)
    at java.base/java.nio.file.Files.walkFileTree(Files.java:2810)
    at java.base/java.nio.file.Files.walkFileTree(Files.java:2881)
    at com.mbm.mediaorganizer.MediaOrganizer.run(MediaOrganizer.java:214)
    at com.mbm.mediaorganizer.MediaOrganizer.main(MediaOrganizer.java:63)

From the call stack you can see that the last line of my code that was executed before entering into publicly available jar files is:

(RelocateFileVisitor.java:390)

But line 390 in my code is within a method that declares that it throws IOException. Why did execution stop on line 390 and not percolate up the call stack?


Solution

  • It did.

    exceptions occur in a few steps.

    An exception object is created and a stack trace attaches.

    At some point the exception is made. As it is made, the stack trace attaches then and there - the stack trace represents that line of code that executes new IOException("Some message here"). That's slightly oversimplified; you can cause the stack trace to represent whatever you want, but, the vast, vast majority of exceptions are not created with custom traces nor do they run methods to update/rewrite the stack trace list.

    That act just makes an object. It has nothing to do with throws statements nor with problems perse... it's just a java object. There is no meaningful difference between the statement new IOException() and the statement new ArrayList(). You can call either whenever you want, and you e.g. do not need to add throws IOException to the method to do it. Try it:

    void foo() {
      IOException e = new IOException();
    }
    

    compiles and runs just fine. It does nothing, of course: We made an exception object. That's all we did. When the method exits, it becomes garbage and is eventually collected same as any other object.

    The exception is thrown

    Throwing an exception merely triggers the percolation effect: The current method (the one that executed the throw statement, which is how you throw exceptions) stops execution and jumps to the nearest catch block that covers the type of the exception you are throwing; it's like a goto. If there is no such block, instead the method exits abruptly (just like return), and then acts like the caller of the method that had the throw statement had actually invoked throw thatException; instead of callMethodThatThrowsIt(); - this is the thing where exceptions 'bubble up', destroying all methods on the stack until somebody catches it.

    Notably this does not have any effect on the stack trace in any way. throwing it doesn't. catching it doesn't.

    Of course, 99% of all throws look like throw new IOException(); - step 1 (make the exception and attach a stack trace) and step 2 (throw it) are the same line.

    The exception is caught

    At some point the bubble-up causes the exception to arrive at catch (IOException e). This will definitely occur, because even main itself is not actually the 'bottom' of the callstack. It merely looks like it. The JVM itself ran main and will catch anything and act accordingly. This is what it looks like:

    class JvmBoot {
      static void actualInitialStartPoint() {
        try {
          MainClass.main(args);
        } catch (Throwable t) {
          Thread.getUncaughtExceptionHandler().handle(t);
        }
      }
    }
    

    Hence, all exceptions end up being caught at some point. And the default uncaught exception handler simply prints, and as it's the 'bottom' of the thread call stack, it's the last thing that thread does; once a thread finishes execution it 'dies' and that is why when you see a stack trace, the app exits (because your main thread was the only non-daemon thread running, its entire stack got winded down to the very very bottom where the exception was caught, dealt with by printing it to syserr, and then that very bottom method on the stack finished, so, the thread dies, and with that, no more threads, and therefore, JVM exits).

    This gets us to the answer:

    Option 1: You DID let it percolate!

    The stack trace indicates that the call stack looked like the stacktrace at the time the expression new IOException() was executed. This was executed by method java.base/java.io.FileInputStream.readBytes(Native Method) and the trace shows you the route that got the JVM to calling that method. Presumably that same method instantly threw it, and this caused the exception to bubble up, insta-returning every method in that stack until a catch block. Which isn't in the code you pasted but closer to the entrypoint (to main / the JVM runner), probably if that showed on the console, all the way to the very top, the JVM runner itself, catching any and all exceptions that main itself ends up bubbling up.

    Option 2: Somebody wrote bad code

    You're free to write this:

    public void methodInALibraryYouAreCalling() {
      try {
        doSomeStuffWithFiles();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    

    This code will do stuff with files, and if it works out, ends 'normally' (no exception bubbling up). If it does not work out, the exception is caught. The handler (the catch block) prints the exception type, message, and trace, and then exits the method normally - the exception has been handled, therefore, no, it does not bubble up. If you're the one calling methodInALibraryYouAreCalling then there is nothing you can do to get at it.

    That's kinda stupid. Which is why the above code is not, at all, how you should ever write java code. A catch block should handle an exception, and 'log it then just continue blindly on as if nothing went wrong' is not handling it!

    If you can't be bothered with writing a sane block, the best 'whatever I do not want to think about it' catch block is this:

    try {
      code here
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
    

    Because while not great at least it keeps bubbling.

    If this happens, you also see the stack trace printed to syserr (that's what e.printStackTrace() does!), and your code cannot tell the difference so continues on, because how could it not?

    The likely answer

    The only code that can have this "I caught it, logged it, and carried blindly onwards" mistake in it is 'above' your code, so it can only be Apache Tika or the java core libraries and I rather doubt they'd write such bad code. Which means it's the first explanation: The exception did propagate. What you see captures the point at which new IOException() was executed; that is the trace and it is not modified as it propagates.

    What did you expect to see instead?