Home > Blockchain >  Stop exception 'factory' methods showing up in stack trace
Stop exception 'factory' methods showing up in stack trace

Time:09-25

Sometimes, rather than creating an exception directly with new, I want to delegate creating the exception to some sort of factory or builder method.

For example, I'm creating an Exception class that uses Lombok's @Builder:

@Builder public class MyException extends Exception

In code throwing exception using build():

throw MyException.build();

The build() method shows up in the stack trace (@Builder is line 9):

exception.MyException$MyExceptionBuilder.build(MyException.java:9)

Obviously Lombok generate build method with return new MyException, but it's irrelevant in stack trace

Can builder method be removed from stack trace? Otherwise, when looking at console output it appears as if the build method failed.

Is it a known code smell or is there a better way to use builder for Exception?

CodePudding user response:

The problem, as you have found, is that Throwables' stack traces get determined when they are constructed, not when they are thrown. This means that any method that acts as a Throwable factory ends up in the stack trace, unless you do something to explicitly remove it.

There are a number of options to work around this. Probably the simplest one is the following:

throw (MyException) MyException.build().fillInStackTrace();

The fillInStackTrace() method is a public method on Throwable. It's the same method that the constructors of Throwable use to populate the stack trace initially, but it can be called again somewhere else to overwrite it. Calling it on the same line as your throw statement will cause the stack trace to be set as desired.

However, there are a number of disadvantages:

  • It relies on all users of the MyException class to remember to do this whenever they throw exception. If they forget, the stack trace will be wrong.
  • There's an unsightly cast required
  • The stack trace is effectively populated twice (once when the exception is constructed, and once when you call fillInStackTrace()). This is a native method and likely to be slow, so there's a performance hit here (although you could argue that since you're in an 'exceptional' circumstance, you don't care that performance isn't optimal).

I would argue that a better approach, that fixes all these disadvantages, would be something like this:


// Remove the @Builder annotation from the exception class,
// and instead bundle all of the complex details in a "details"
// object that is passed to the exceptions constructor.
//
// You can either save the details object as a field (as below),
// extracting the relevant info in the exception's methods,
// or extract everything in the constructor body.

@RequiredArgsConstructor
class MyException extends Exception {
    private final ExceptionDetails details;
}

// Instead, the details class has the builder

@Builder class ExceptionDetails {}

// You can now throw exceptions like this:

throw new MyException(ExceptionDetails.builder()/*...etc...*/.build());

With this approach, since the exception constructor is called directly on the same line as throw the stack trace will automatically be filled in as expected. There's no casting required, and there's nothing special that developers have to remember to do when using your exception class – use of the details object and builder is somewhat unusual and special, but they're forced and reminded to do it by the constructor signature.

CodePudding user response:

I use something like this to strip unwanted stack trace elements when delegating exception creation logic out of the method that would naturally throw it.

/**
 * <p>Changes the stack trace of exception (if it has stack trace) so that it ends with last
 * entry before entering a method of the class given with <code>className</code> argument.</p>
 * <p>If the <code>className</code> is not found within stack trace it is unchanged.</p>
 * <p>The method is intended to be used with exception utility and factory classes to avoid
 * polluting stack trace with unneeded entries.</p>
 * @param throwable {@link Throwable} to edit, cannot be null
 * @param className {@link String} as returned by {@link Class#getName()}, cannot be null
 * @param <T> type of {@link Throwable} argument
 * @return {@link Throwable} that was passed in as throwable argument
 */
    public static <T extends Throwable> T reduceStackFrame( T throwable, String className ) {
        StackTraceElement[] trace = throwable.getStackTrace();
        int length = trace.length;
        if( length != 0 ) {
            int reduction = -1;
            for( int i = length - 1; i >= 0; --i ) {
                if( className.equals( trace[i].getClassName() ) ) {
                    reduction = i   1;
                    break;
                }
            }
            if( reduction > 0 ) {
                StackTraceElement[] reduced = new StackTraceElement[length - reduction];
                System.arraycopy( trace, reduction, reduced, 0, length - reduction );
                throwable.setStackTrace( reduced );
            }
        }
        return throwable;
    }

Advantage is that it can be just called from your factory method on return statement without requiring any changes in existing user code.

Note that this approach will not work if stack trace is not writable (it will not be changed).

  • Related