System.debug() writes a message to the debug log at the point where it’s called. It’s the lowest-friction way to see what your Apex code is doing at runtime — what variables hold, which branches run, where exceptions originate. Every Apex developer uses it. Most overuse it.
The basic usage
System.debug('Hello');
System.debug('account count = ' + accs.size());
System.debug(myAccount); // sObjects, lists, maps all stringify automatically
Output lands in the debug log as a USER_DEBUG event with the source line number and the message you passed.
With an explicit logging level
System.debug(LoggingLevel.WARN, 'unexpected null industry on account ' + a.Id);
System.debug(LoggingLevel.ERROR, 'callout failed: ' + e.getMessage());
LoggingLevel values: ERROR, WARN, INFO, DEBUG (default), FINE, FINER, FINEST. The platform only outputs a message if the trace flag’s level for the Apex Code category is at or above the message’s level.
What it’s good at
- Variable inspection — print a value, see what it is.
- Branch tracing —
System.debug('hit if branch')confirms execution reached a particular point. - Loop visibility — print each iteration’s key values.
- Exception logging —
System.debug(e.getStackTraceString())in a catch block. - Pre/post DML state — print a record before insert, then after.
What it’s not good at
- Replacing a debugger. It’s printf-style debugging. For complex execution paths, the Apex Replay Debugger in VS Code is better.
- Production logging. Debug logs are ephemeral and require a trace flag. Real production logging goes to Platform Events, a custom
Log__cobject, or a third-party tool like Pharos or Logger. - Long-running batches. Heavy
System.debugin a 100-execution batch fills the 20 MB log cap.
A common pattern
Trace at the boundaries of every method:
public Decimal computeTax(Account a, Decimal amount) {
System.debug('computeTax IN: a=' + a.Id + ' amount=' + amount);
Decimal rate = lookupRate(a.BillingCountry);
Decimal tax = amount * rate;
System.debug('computeTax OUT: rate=' + rate + ' tax=' + tax);
return tax;
}
When something goes wrong, you see the inputs and outputs of every method on the call path.
Performance impact
System.debug is not free. The platform still has to format the string and write it to the log, even if the trace flag is disabled. In tight loops:
for (Account a : Trigger.new) {
System.debug('processing ' + a); // BAD if Trigger.new has 200 records
// ...
}
This adds milliseconds per call and 100s of KB to the log. Two mitigations:
- Guard with a class-level flag —
if (Logger.enabled) System.debug(...);so production turns it off entirely. - Use higher levels —
System.debug(LoggingLevel.FINEST, ...)so the platform skips formatting when the trace level is below FINEST.
Logger pattern in mature codebases
public class Logger {
public static void info(String msg) {
System.debug(LoggingLevel.INFO, msg);
}
public static void error(String msg, Exception e) {
System.debug(LoggingLevel.ERROR, msg + ' :: ' + e.getMessage() + '\n' + e.getStackTraceString());
// Also: insert Log__c, or EventBus.publish a System_Log__e
}
}
Logger.error('failed to process order', e);
This centralizes the message format, makes “where do logs go?” a single config decision, and lets you swap to platform-event-based logging without rewriting every callsite.
When to remove System.debug
Generally: never, but tune the level. The accepted view is:
- Keep
LoggingLevel.INFOand above messages in production code — they help when you need to triage later. - Don’t ship
LoggingLevel.DEBUGorFINESTcalls with expensive string concatenation — wrap them in a flag check. - Remove temporary debug prints added during a bug hunt — they make the code noisy.
Common antipatterns
// BAD — formats the string even when the trace is off
System.debug('account=' + JSON.serialize(a));
// BETTER — formats lazily only when DEBUG level is on
if (Logger.debugEnabled) System.debug('account=' + JSON.serialize(a));
// BAD — debugging inside a tight bulk loop with no level guard
for (Order o : orders) System.debug(o);
// BAD — printing entire collections
System.debug(Trigger.new); // serializes all 200 records
What interviewers are really looking for
The basic answer is “write to the debug log to trace what code did.” Strong signals: (1) logging levels and how the trace flag’s category-level filters control output, (2) the cost — formatting still happens even when output is filtered, so guard expensive calls, (3) production logging belongs in a Log__c object or Platform Event, not raw System.debug, (4) Apex Replay Debugger is a step up from printf-style debugging for complex bugs.
Verified against: Apex Developer Guide — Using System.debug, Debug Log Levels. Last reviewed 2026-05-17.