2

I am trying to capture an error thrown from Apex in my LWC promise chain, with an end goal to publish a Slack message with the error details. The slack message template contains the following needed fields:

  • Status Code
  • Error Message
  • Error Stack Trace When using catching the exception and rethrowing it as AuraHandledException, the stack trace is only intermittently populated and the error structure changes depending on the error type.

Here is console log example using AuraHandledException with stack trace being displayed:

{
  "status":500,
  "body":{
    "exceptionType":"System.LimitException",
    "isUserDefinedException":false,
    "message":"Too many DML statements: 1",
    "stackTrace":"Class.MyController.myMethod: line 32, column 1"
},
  "headers":{},
  "ok":false,
  "statusText":"Server Error",
  "errorType":"fetchResponse"
}

Here is another example using AuraHandledException, but the stack trace is not displayed:

{
  "status":500,
  "body":{
    "message":"Insert failed. First exception on row 0; first error: REQUIRED_FIELD_MISSING, Required fields are missing: [My Field]: [My Field]"
  },
  "headers":{},
  "ok":false,
  "statusText":"Server Error",
  "errorType":"fetchResponse"
}

Here is another example without using try/catch in apex (only capturing in LWC promise). This one displays the stack trace:

{
    "status":500,
    "body":{
        "exceptionType":"System.LimitException",
        "isUserDefinedException":false,
        "message":"Too many DML statements: 1",
        "stackTrace":"Class.MyController.myMethod: line 31, column 1"
    },
    "headers":{},
    "ok":false,
    "statusText":"Server Error",
    "errorType":"fetchResponse"
}

Here is another example without try/catch in apex, that does not display the stack trace and the whole error structure changes:

{
    "fieldErrors":{
        "Company":[{
                "statusCode":"REQUIRED_FIELD_MISSING",
                "message":"Required fields are missing: [Last Name, Company]"
            }]
    },
    "pageErrors":[{
        "statusCode":"REQUIRED_FIELD_MISSING",
        "message":"Required fields are missing: [Last Name, Company]"
    }],
    "index":null,
    "duplicateResults":[]},
    "headers":{},
    "ok":false,
    "statusText":"Server Error",
    "errorType":"fetchResponse"
}

Here is a example throwing CustomException, the error structure never changes but I cannot use CustomExceptions:

{
    "status":500,
    "body":{
        "exceptionType":"System.LimitException",
        "isUserDefinedException":false,
        "message":"Too many DML statements: 1",
        "stackTrace":"Class.MyController.myMethod: line 33, column 1"
    },
    "headers":{},
    "ok":false,
    "statusText":"Server Error",
    "errorType":"fetchResponse"
}

{
    "status":500,
    "body":{
        "exceptionType":"MyController.MyCustomException",
        "isUserDefinedException":true,
        "message":"Insert failed. First exception on row 0; first error: REQUIRED_FIELD_MISSING, Required fields are missing: [Last Name, Company]: [Last Name, Company]",
        "stackTrace":"Class.MyController.myMethod: line 334, column 1"
    },
    "headers":{},
    "ok":false,
    "statusText":"Server Error",
    "errorType":"fetchResponse"
}

The problem is that I cannot use CustomExceptions, so how can I have the same error structure for different error types, using AuraHandledException? Really appreciate any help.

1 Answer 1

1

The only way to have a consistent stack trace is to be consistent in your code. You'll notice the problems revolve around DMLException. Throwing a naked DMLException gives you that "weird" response with the pageErrors and fieldErrors directly on the top object with no stack trace. Handling a DMLException in a try-catch and converting to an AuraHandledException can have similar challenges.

The only reliably consistent way to get a stack trace is to never use allOrNone=true for any DML operation. Instead, always use allOrNone=false and handle the errors individually, row by row. Do not allow any exceptions to be thrown in a trigger context, always use try-catch or Fault elements (for Flows). Always communicate errors to a parent transaction using addError instead of throwing exceptions.

Note that LimitException always has a stack trace, as far as I'm aware, which is why the try-catch doesn't matter. DMLException, by contrast, never has a stack trace, at least not since a few years ago when they did some major overhaul work on DML transactions. In previous releases, a full stack trace would appear in a DMLException, but this hasn't worked for years now, presumably because exceptions were leaking incorrectly and provided an inconsistent experience.

For example, it used to be that the follow code worked as expected:

trigger X on Y (after insert) {
  List<String> values = new List<String>();
  values[0] = 'Hello World';
}
try {
  insert new Y();
} catch(ListException e) {
  System.debug(e.getStackTraceString()); // Useful output here
}

But, after this was fixed, the above code instead threw a DMLException, and thus crashed.

To improve reliability, any error that crosses a DML statement is now a DMLException:

try {
  insert new Y();
} catch(DMLException e) {
  System.debug(e.getStackTraceString()); // No stack trace info here...
}

This means that, to have this consistent experience, you must refactor any code that does not apply to the two rules I specified above (allOrNone=false and exclusively use addError). Outside of a trigger, use AuraHandledException to rollback the entire transaction and notify the client of an error.

This design will have the added benefit of removing most of the try-catch statements from your code, since you'll never be throwing exceptions. This makes the code easier to read and write in the long term.

You must log in to answer this question.

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