15

I use a CustomException and can't get the stack trace to work. I meticulously followed and implemented the Salesforce Developer Guide for Custom Exceptions but my debug log simply shows an empty stack trace.

11:30:40:294 USER_DEBUG [8]|DEBUG|Line number: 22
11:30:40:295 USER_DEBUG [9]|DEBUG|Stack trace: ()

This makes no sense at all. If I catch one of the standard exceptions, I can read that property. Posts in developer boards clearly state, that getStackTraceString() should work on both custom and system exceptions. So why is my debug log different? What am I doing wrong?

Code for Custom Exception:

public class MerchandiseException extends Exception {

}

Code that throws the exception:

public class MerchandiseUtility {
    public static void mainProcessing() {
        try {
            insertMerchandise();
        } catch(MerchandiseException me) {
            System.debug('Message: ' + me.getMessage());    
            System.debug('Cause: ' + me.getCause());    
            System.debug('Line number: ' + me.getLineNumber());    
            System.debug('Stack trace: ' + me.getStackTraceString());    
        }
    }

    public static void insertMerchandise() {
        try {
            // Insert merchandise without required fields
            Lead l = new Lead();
            insert l;
        } catch(DmlException e) {
            // Something happened that prevents the insertion
            // of Employee custom objects, so throw a more
            // specific exception.
            throw new MerchandiseException(
                'Merchandise item could not be inserted.', e);
        }
    }
}

I don't have a "Merchandise__c" Object, thats why I decided to create a lead object which will throw the exact same DMLException (since LastName is required and not set). If I change the code to re-throw an arbitrary System Exception (i.e. NullPointerException) and catch a generic Exception in the outer try/catch, debug log works flawlessly:

12:11:08:174 USER_DEBUG [8]|DEBUG|Line number: 22
12:11:08:174 USER_DEBUG [9]|DEBUG|Stack trace: Class.MerchandiseUtility.insertMerchandise: line 22, column 1
12:11:08:000 USER_DEBUG Class.MerchandiseUtility.mainProcessing: line 4, column 1
12:11:08:000 USER_DEBUG AnonymousBlock: line 1, column 1
12:11:08:000 USER_DEBUG AnonymousBlock: line 1, column 1
2
  • 1
    Probably best you show the code that does the logging of the exception. I presume this is not an installed managed package - by default line-level detail is hidden for those.
    – Keith C
    Apr 25, 2018 at 10:00
  • Sorry, I thought it to be clear that I line-by-line copied the code snipptes from Developer Guide. Will add the snippets in the primary post. Apr 25, 2018 at 10:03

4 Answers 4

9

Update - the known issue was resolved in the Spring `22 release.

Both debug statements in the following anonymous Apex example will produce the stack trace.

class myexception extends exception{}
try {
  throw new myexception();
} catch(exception e) {
  System.debug(e.getStackTraceString());
}
System.debug((new myexception()).getStackTraceString());

10:47:47.190 (194832907)|USER_DEBUG|[5]|DEBUG|AnonymousBlock: line 3, column 1 10:47:47.190 (194981132)|USER_DEBUG|[7]|DEBUG|AnonymousBlock: line 7, column 1


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.

2
  • 1
    It is now fixed. They fixed it in Spring '22
    – Leto
    Nov 23, 2022 at 20:21
  • 1
    @Leto Thanks for pointing this out. I've updated the answer to reflect the fix what was released as part of Spring `22. Nov 25, 2022 at 18:50
11

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;
    }
}
7

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(); 
    }
}
7
  • I'm sure theres other data stored that might be accessible but I really can find very little on using inheritance properties with custom exceptions. Apr 26, 2018 at 17:52
  • 1
    this is very useful yet astonishing that getStackTraceString() is not available for thrown custom exceptions
    – cropredy
    Aug 30, 2018 at 1:29
  • 4
    N.B. if custom exception is instantiated with a string rather than an exception, getCause() returns null and AFAIK, no way to get stacktraceString
    – cropredy
    Aug 30, 2018 at 2:11
  • This gives me null pointer exception for this line "super.getCause().getStackTraceString()" Aug 7, 2019 at 13:44
  • 1
    See cropredy's comment above. If you have a new question feel free to ask a new question Aug 7, 2019 at 13:54
-2

The answer Aidan gave should be voted much higher! I just ended up tackling this myself from not seeing his answer when I checked this page...

There's a gist of it that you can grab here: https://gist.github.com/jmather/8be36c901aa1242c6bd65acb8003f009

First, since we can't (at least now?) override the getStackTrace() and getStackTraceAsString(), we make a new custom exception class where we can set the stack trace to report:

global with sharing class Sentry_Exception extends Exception {
    global Map<String, Object> context = new Map<String, Object>();
    private List<String> stackTrace;

    global void setStackTrace(List<String> lines) {
        stackTrace = lines;
    }

    global String getCustomStackTraceAsString() {
        return String.join(getCustomStackTrace(), '\n');
    }

    global List<String> getCustomStackTrace() {
        if (stackTrace != null) {
            return stackTrace;
        }

        return new List<String>();
    }
}

And then we make a unified way to create any kind of exception instance...

global with sharing class Sentry_ExceptionFactory {
    global static Sentry_Exception build() {
        return (Sentry_Exception) build(Sentry_Exception.class);
    }

    global static Exception build(String exceptionClass) {
        Type exType = Type.forName(exceptionClass);
        return build(exType);
    }

    global static Exception build(Type exType) {
        try {
            Sentry_Exception ex = (Sentry_Exception) exType.newInstance();
            try {
                SObject foo = null;
                Id fooId = foo.Id;
            } catch (Exception e) {
                String stack = e.getStackTraceString();
                List<String> lines = stack.split('\n');
                lines.remove(0);
                lines.remove(0);

                ex.setStackTrace(lines);
                return ex;
            }
        } catch (Exception e) {
        }

        try {
            Exception ex = (Exception) exType.newInstance();
            return ex;
        } catch (Exception e) {
        }

        Sentry_Exception ex = (Sentry_Exception) Sentry_ExceptionFactory.build(Sentry_Exception.class);
        ex.setMessage('Could not instantiate given class as an exception!');
        ex.context.put('exceptionClass', exType.getName());
        return ex;
    }
}

I went with this approach so that you could use the same pattern for creating an exception throughout a codebase.

This is part of a "Sentry for Salesforce" library I am working on, hence the "Sentry_" prefix.

1

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .