Nested Traces and the Two Modes

Apex Stem Docs
Apex StemApexTraceInternalsSalesforceApex
The LIFO ordering rule when a Usecase calls a Usecase, plus the Strict mode that switches on automatically under test versus the Relaxed mode that keeps production running.

Nested Traces and the Two Modes

When one Usecase calls another, the Trace nests. ApexTrace manages that nesting as a strict LIFO stack and throws on any inconsistency.

The Outer / Inner ordering rule

The rule for nested traces is: close the Inner before you close the Outer.

APEX
Trace outer = Trace.of('Outer'); outer.start();
  Trace inner = Trace.of('Inner'); inner.start();
    inner.log('inside inner');     // ✅
  inner.finish();                  // ✅ Inner closes first
outer.finish();                    // ✅

Call the Outer's log / finish after the Inner has started but before it closes with finish / skip / abort, and Strict mode throws immediately.

Strict mode and Relaxed mode

TraceFlow has two modes, switched automatically by execution context.

ModeDefault switchBehaviour
StrictUnder test (Test.isRunningTest() == true)Throws immediately on nesting or ordering violations. Catches bugs early
RelaxedIn productionAbsorbs minor inconsistencies by aborting automatically. Production is less likely to fall over

It is tempting to assume "green under test means fine in production" — keep in mind that behaviour differs by mode.

⚠️ The switch is driven solely by Test.isRunningTest(). There is no API for switching it explicitly (changeModeTo and TraceMode are both private).

Trace.of() belongs in the instance field initialiser, once

Call Trace.of(...) exactly once, when the Usecase class's instance field is initialised. Re-create it inside invoke() and you get a fresh Trace on every call, which breaks the nesting and aggregation logic.

APEX
public with sharing class YourUsecase {
  private Trace t = Trace.of('The process name');  // ✅ once, in the instance field
 
  public void invoke() {
    // private Trace t = Trace.of(...);  // ❌ never re-create it inside invoke
    this.t.start();
    // ...
  }
}

Why tests open with Trace.of('...').start()

Every test example so far starts with these two lines.

APEX
Trace t = Trace.of('Normal: ...');
t.start();

and calls t.finish(); at the end. There is a primary reason and a side benefit.

Primary reason: to catch a broken Trace lifecycle inside the Usecase. If the Usecase returns without calling finish / skip / abort, an unbalanced "still open" trace is left on the context. Wrapping the test in an outer Trace lets Strict mode (the default under test) surface that mismatch at test time, so you find the missing exit before it reaches production.

Side benefit: it resets the previous trace state. Any context left behind by the previous test is explicitly unwound at the top of the new one.

The shape of a whole test class

Every test method gets it, at the top and at the bottom. Across a class it looks like this.

APEX
@isTest(seeAllData=false)
private class ResummarizeUsecase_T {
  @isTest
  static void testInvoke_WhenOpportunitiesExist_ThenSummaryUpdated() {
    Trace t = Trace.of('Normal: the account summary is updated when opportunities exist');
    t.start();
 
    // Arrange
    ...
 
    // Act
    ...
 
    // Assert
    ...
    Assert.isTrue(TraceFlow.isLastFinish());
 
    t.finish();
  }
 
  @isTest
  static void testInvoke_WhenNoOpportunities_ThenSkipped() {
    Trace t = Trace.of('Normal: it skips when there is not a single opportunity');
    t.start();
 
    // Arrange
    ...
 
    // Act
    ...
 
    // Assert
    Assert.isTrue(TraceFlow.isLastSkip());
 
    t.finish();
  }
}

The point is that t.start() and t.finish() bracket the method. Once the shape is in place, the moment an inner Usecase forgets its exit, Strict mode catches it.

A second benefit: describing the test outside the naming convention

Test method names are bound to a shape such as test{Method}_When{condition}_Then{result}. That is a correct constraint for an identifier you want to grep — but inside an identifier, both the length and the vocabulary available to you are limited.

The argument to Trace.of(...) is just a string, so it sits outside that constraint.

RoleConstraint
Method nameMachine-readable. The identifier you grep and see in resultsNaming convention, identifier-legal characters
Trace.of argumentHuman-readable. What the test actually verifiesNone

For teams outside the English-speaking world this translates directly into readability. The method name stays in English per the convention, while the description is written in your own language. Reading "what is being verified" in your first language is measurably faster in review and in incident triage — and it costs nothing in terms of the naming convention.

The string is also written to the debug log, so when a test fails the description is right there in the log.

📌 The Apex Stem convention starts this string with a category label: Normal: / Abnormal: / Edge case:. Put the label first, then the human-readable description. (Japanese projects use 正常系: / 異常系: / エッジケース: — the point is the label, not the language.)