Get StackTrace in Custom Exception

You can actually work around the problem of custom exceptions failing to provide stacktraces...

Here's an abstract class that you can extend instead of Exception itself:

public abstract class StackTraceException extends Exception {

    private String stackTrackString = createStackTraceString();

    private String createStackTraceString() {
        stackTrackString = new DmlException().getStackTraceString();
        // The first thee lines are this method, the Exception constructor, and the subclass. So, discard them
        return stackTrackString.replaceFirst('(?m)(.*\\n){3}?', '');
    }

    public override String getStackTraceString() {
        return stackTrackString;
    }
}

You can't override the constructor in an exception class, but you can get create with variable initialisers to call createStackTraceString() at the point where the exception is created. This then works the way you would want...

@IsTest
private class StackTracesTest {

    private final static String MAGIC_STRING = 'magic string!';

    class MyException extends StackTraceException {
        public override String getMessage() {
            return MAGIC_STRING;
        }
    }
    @IsTest static void testGetErrorMessageWrapped() {
        Exception testException;

        try {
            try {
                insert new Contact();
            } catch (Exception e) {
                throw new MyException(e);
            }
        } catch (MyException e) {
            testException = e;
        }

        List<String> messages = StackTraces.getErrorMessage(testException);
        String joinedMessages = String.join(messages, '\n');
        System.debug('\n' + joinedMessages);
        System.assert(joinedMessages.contains(MAGIC_STRING));
        System.assert(joinedMessages.contains(StackTraces.class.getName()));
        System.assert(joinedMessages.contains('REQUIRED_FIELD_MISSING'));
    }
}

The debug output has all the details and correct line numbers for the both the DMLException where the Contact fails to insert and the line where it is rethrown as MyException.

It has the possibility of being wrong because we're assuming that you throw from the same line as the construction of your custom exception. But, you almost always construct and throw on the same line.

For completeness StackTraces is a utility for digging into the causes:

global class StackTraces {

    /**
     * Takes an Exception which may have a cause exception inside it, and builds a string of all messages + traces
     * inside it by calling getCause() until there are no more.
     *
     * @param e an Exception
     * @return the type, messages, and stack traces from e and all nested Exceptions inside it
     */
    global static List<String> getErrorMessage(Exception e) {
        List<String> returnVal = new List<String>();
        Integer exceptionCount = 0;
        do {
            returnVal.add(String.format('type[{0}]: {1}', new List<Object>{ exceptionCount, e.getTypeName()}));
            returnVal.add(String.format('message[{0}]: {1}', new List<Object>{ exceptionCount, e.getMessage()}));
            returnVal.add(String.format('stack trace[{0}]:\n{1}', new List<Object>{ exceptionCount, e.getStackTraceString()}));
            e = e.getCause();
            ++exceptionCount;
        } while(e != null);
        return returnVal;
    }
}

There is the existing known issue Exception.getStackTraceString() does not work for custom exceptions with Spring'16.

Summary
Exception.getStackTraceString() doesn't return a string when used in Custom exceptions

Sadly, when it was last updated 2018-10-13 it was marked as "NO FIX". No reason is given.


You can access inherited variables and methods using the super keyword in an override method. Since you've inherited an exception, you have access to getStackTraceString() and getCause() inside override methods.

You can use these methods to do further debugging to determine where exactly has the data you're looking for. See the below snippet:

public class DummyException extends Exception {
    public override String getStackTraceString() {
        return super.getCause().getStackTraceString(); 
    }
}

By accessing getCause, we get the original exception & its methods, allowing us to call getStackTraceString() and return that value. You could also try super.GetStackTraceString().

Heres how these values output on my system (cs14):

enter image description here

Removing the override for getStackTraceString returns an identical string as super.getStackTraceString. I can't repo an exception returning an empty stacktrace string, but there's always a suspicious () and a few lines of whitespace when calling getStackTraceString on a custom exception.

Class used for testing:

public class DummyExe {

    public static void DoSomething() {
        DoSomethingElse(); 
    }

    private static void DoSomethingELse() {
        try {
            insert new Account();
        } catch (Exception e) {
            System.debug('Provided Exception: ');
            System.debug(e.getStackTraceString());

            System.debug('Custom Exception: ');
            System.debug(new DummyException('...', e).getStackTraceString());
        }
    }

}

Exception class w/ various method calls:

public class DummyException extends Exception {
    public override String getStackTraceString() {

        System.debug('super.getCause().getStackTraceString()');
        System.debug(super.getCause().getStackTraceString());

        System.debug('super.getStackTraceString()');
        System.debug(super.getStackTraceString());

        return super.getCause().getStackTraceString(); 
    }
}

Tags:

Exception

Apex