Testing @AuraEnabled Methods from Anonymous Apex
Overview
During development, you often need to quickly test @AuraEnabled Apex methods without deploying a full LWC front end. Anonymous Apex is the natural tool for this, but two platform restrictions get in the way: AuraHandledException cannot be thrown outside the Aura/LWC context, and HTTP callout methods fail if any DML has occurred in the same transaction.
This article presents a lightweight test harness pattern that works around both issues, enabling rapid testing and debugging of @AuraEnabled methods directly from the Developer Console. It covers the harness class design, usage examples, callout-safe execution, and cleanup practices to keep the harness out of production.
Testing @AuraEnabled Methods from Anonymous Apex
When building LWC-backed Apex controllers, you often need to test methods in isolation — verifying API callouts work, checking return shapes, or debugging specific inputs. Anonymous Apex is the fastest way to do this, but @AuraEnabled methods introduce two friction points that require a workaround.
Problem 1: AuraHandledException Outside Aura Context
Most @AuraEnabled methods wrap errors in AuraHandledException to surface clean messages to the LWC:
@AuraEnabled
public static String doSomething(String input) {
try {
// business logic
} catch (Exception e) {
throw new AuraHandledException(e.getMessage());
}
}
Calling this from Anonymous Apex works fine on the happy path. But if it throws, you get:
System.LimitException: Can only throw this exception type from VisualForce or Aura context
This is a runtime restriction — AuraHandledException can only be thrown within the Aura/LWC execution context. Anonymous Apex does not qualify, so the exception itself causes an unrecoverable LimitException.
Problem 2: DML-Before-Callout
If the method under test makes HTTP callouts, and your anonymous script performs any DML beforehand (even setting up test data), you will hit:
System.CalloutException: You have uncommitted work pending
The Solution: Test Harness Class
Create a lightweight wrapper class that catches AuraHandledException before it propagates and returns structured results instead:
public class DevTestHarness {
public static Map<String, Object> testAuraMethod(String methodName, Map<String, Object> params) {
Map<String, Object> result = new Map<String, Object>();
try {
switch on methodName {
when 'getAccountDetails' {
String accountId = (String) params.get('accountId');
Object data = AccountController.getAccountDetails(accountId);
result.put('success', true);
result.put('data', data);
}
when 'sendChatMessage' {
String message = (String) params.get('message');
String sessionId = (String) params.get('sessionId');
Object data = ChatController.sendMessage(message, sessionId);
result.put('success', true);
result.put('data', data);
}
when else {
result.put('success', false);
result.put('error', 'Unknown method: ' + methodName);
}
}
} catch (AuraHandledException e) {
// Caught before it propagates — safe in any context
result.put('success', false);
result.put('error', e.getMessage());
result.put('exceptionType', 'AuraHandledException');
} catch (Exception e) {
result.put('success', false);
result.put('error', e.getMessage());
result.put('exceptionType', e.getTypeName());
}
return result;
}
}
Usage from Anonymous Apex
Map<String, Object> params = new Map<String, Object>{
'accountId' => '001xx000003DGXYZ'
};
Map<String, Object> result = DevTestHarness.testAuraMethod('getAccountDetails', params);
System.debug(JSON.serializePretty(result));
Output on success:
{
"success": true,
"data": { "Name": "Acme Corp", "Industry": "Technology" }
}
Output on error (safely caught):
{
"success": false,
"error": "Account not found or insufficient permissions",
"exceptionType": "AuraHandledException"
}
Handling Callout Methods
For methods that make HTTP callouts, avoid any DML in your anonymous script before calling the harness. If the method itself does DML before the callout, that is an internal design issue (see the tool calling pattern for the correct architecture).
If you need test data, create it in a separate anonymous execution first, note the record IDs, then run the harness in a second execution:
// Execution 1: Create test data
Contact c = new Contact(LastName = 'Test', Email = 'test@example.com');
insert c;
System.debug('Contact ID: ' + c.Id); // Copy this ID
// Execution 2: Test the callout method (no DML in this transaction)
Map<String, Object> params = new Map<String, Object>{
'contactId' => '003xx000004ABCDE' // Paste ID from execution 1
};
Map<String, Object> result = DevTestHarness.testAuraMethod('sendWelcomeEmail', params);
System.debug(JSON.serializePretty(result));
When to Use This Pattern
- Live API testing: Verify callout integrations return expected data without deploying an LWC.
- Debugging production issues: Reproduce a specific error by calling the method with the exact parameters a user triggered.
- Integration testing: Confirm end-to-end flows before wiring up the front end.
- Data exploration: Call
@AuraEnabled(cacheable=true)query methods to inspect data shapes.
Cleanup
The harness class is a development tool. Exclude it from production deployments by keeping it in a scratch-org-only or sandbox-only package directory, or gate it behind a custom permission check:
if (!FeatureManagement.checkPermission('Dev_Test_Harness_Access')) {
throw new SecurityException('Harness access denied');
}
This ensures the harness is never callable in production, even if it accidentally gets deployed.