Search code examples
javascalaexceptionargumentscoredump

Get arguments from a scala exception


Suppose that you have an exception in Scala (or Java). How can you get the arguments of the methods in the stack trace?

The arguments contain information that would be invaluable when diagnosing faults. Alas I see no way of getting them even though I believe that they should still be in memory.

catch(e) {
    x.getStackTrace.map(level => {
        // level has such methods as getClassName and getMethodName
        // but not the arguments.
        // Maybe the information is somewhere completely different?
        // In a Java equivalent of a core dump perhaps?
    })
}

Solution

  • You cannot, at least not in normal program execution.

    When an exception is thrown, the stack is "unwound" and execution resumes at the nearest applicable "catch" block. Any data on the stack (including method arguments) is lost to the program and immediately available for GC.

    If the program is being debugged, then you could set a breakpoint when the exception is thrown and inspect the method arguments in the debugger. (You could probably achieve the same effect using a JVM Agent.)

    You could of course pass the method arguments into the exception constructor.

    See:

    Follow-up questions: could we change Java to do this?

    You said:

    It looks as if there is an opportunity to make this better by changing the way exceptions are handled - by capturing the information before unwinding.

    and then

    How is the stack allocated in the JVM? What details of the method make it hard to get the arguments? If it is similar to stacks in say C then it is not obvious to me that it is necessary to unwind before executing the exception handler. Can not the handler run on the top of the stack or in a second stack with viewing rights to the first?

    As I noted below, I think this is unlikely to change in the near future because of the way the stack is allocated on a standard JVM. Your two suggested alternatives are good questions and I think allow us to see why the current approach is used.

    1. Can not the handler run on the top of the stack?

    It could, of course. The main drawback to that would be that the stack would grow by a lot each time that an exception is handled.

    Take the following example code:

    public Foo computeFoo() {
      try {
        // 1
        return firstVersion();
      } catch (Exception e) {
        // 2
        return secondVersion();
      }
    }
    

    Say that we arrived at point "1" via some method calls a, b, c. The stack of stack frames might then look like:

    [ a, b, c, computeFoo ]
    

    Let's say that firstVersion invokes methods x, y, z, and an exception is thrown inside "z". At the moment the exception is thrown, the stack might look like:

    [ a, b, c, computeFoo, firstVersion, x, y, z ]
    

    When we move to point 2, a traditional JVM would be able to discard all the data from x, y, z instantly and simply by truncating the stack before moving into secondVersion

    [ a, b, c, computeFoo, secondVersion ]
    

    Under your proposal, the JVM would need to retain the stack frame data from x, y and z on the stack, in case any code near "2" wanted to access the parameters:

    [ a, b, c, computeFoo, firstVersion, x, y, z, secondVersion ]
    

    It should be clear that, in general, this may result in a lot more data being stored on the stack in programs that use exceptions than needs to be at present. There would also be some extra book-keeping needed in the JVM to handle the "mixed" stack of in-use and preserved stack frames, which will complicate maintenance and slow things down.

    Given that a) all parameter data is not usually needed by exceptions and b) there are easy workarounds for capturing specific parameter data when it is needed, the current standard tradeoff here seems better.

    1. Can not the handler run in a second stack with viewing rights to the first?

    It could, of course, but that would be a great deal more work for the JVM to implement, which would slow everything down for a benefit that is not particularly compelling. Hence the current tradeoff.

    It is always worth bearing in mind that these systems are not "handed down from the gods", but have been designed by people and are the result of years of evolution, consensus-building and tradeoff. One of the most important reasons that the JVM works like this is because C++ used to work like this and that is what most people involved expect and understand.