pinpoint-apm/pinpoint · error · IllegalStateException

cannot leave with trace scope. depth: ${depth}

Error message

cannot leave with trace scope. depth: ${depth}

What it means

BoundaryTraceScope.leave() throws IllegalStateException when the scope cannot be legally exited: either the scope is not active (depth <= 0) or nested boundary counting is inconsistent (skippedBoundary != 0 or depth != 1). Boundary scopes track nested entry/exit pairs for a trace; leave() is only valid when exactly one unmatched entry exists. The guard protects the profiler's scope invariant that pushes and pops are balanced.

Source

Thrown at agent-module/profiler/src/main/java/com/navercorp/pinpoint/profiler/context/scope/BoundaryTraceScope.java:59

            return false;
        } else {
            depth++;
            return true;
        }
    }

    public boolean canLeave() {
        if (skippedBoundary == 0 && depth == 1) {
            return true;
        } else {
            skippedBoundary--;
            return false;
        }
    }

    public void leave() {
        if (!isActive()) {
            throw new IllegalStateException("cannot leave with trace scope. depth: " + depth);
        }

        if (skippedBoundary != 0 || depth != 1) {
            throw new IllegalStateException("cannot leave with BOUNDARY trace scope. depth: " + depth);
        }
        depth--;
    }

    @Override
    public boolean isActive() {
        return depth > 0;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("DefaultTraceScope{");
        sb.append("name='").append(name).append('\'');
        sb.append(", depth=").append(depth);

View on GitHub (pinned to 744c3d3075)

Solutions

  1. Ensure every scope push/enter has exactly one matching leave in a finally block
  2. Verify push() executes before any early return that later triggers leave()
  3. Check for double-leave (leave called twice for one scope) in the interceptor chain
  4. Review recursive/reentrant instrumented methods to ensure nested boundaries are balanced before leaving

Example fix

// before
TraceScope scope = trace.getScope(SCOPE_NAME);
if (scope != null) {
    scope.leave();
}
// after
TraceScope scope = trace.getScope(SCOPE_NAME);
if (scope != null && scope.isActive()) {
    try {
        // instrumented work
    } finally {
        scope.leave();
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

TraceScope scope = trace.getScope(SCOPE_NAME);
if (scope == null || !scope.isActive()) {
    return; // nothing to leave
}

Type guard

boolean canLeaveSafely(TraceScope scope) {
    return scope != null && scope.isActive();
}

Try / catch

try {
    scope.leave();
} catch (IllegalStateException e) {
    logger.warn("Unbalanced trace scope exit: {}", e.getMessage());
}

Prevention

When it happens

Trigger: leave() is called without a matching push/enter (isActive() false), or called while nested boundary entries remain (depth != 1) or boundary skips are outstanding (skippedBoundary != 0) — typically unbalanced interceptor enter/exit, an exception thrown between push and leave, or double-leave on the same scope.

Common situations: Interceptors that call leave() in a path where push() never ran (e.g. an early return before push), exception thrown mid-interception skipping the balanced leave, or recursive instrumentation entering the boundary twice but leaving only once/incorrectly nested.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of pinpoint-apm/pinpoint@744c3d3075 (2026-09-07). Data as JSON: /api/errors/f394bf85f190c301. Report an issue: GitHub.