Default to the cheapest behavior: gate expensive work behind explicit options or conditions, and keep the parsing “fast path” strictly minimal.

Practical rules:

Example pattern (non-throwing, avoid eager ctx/allocation):

_parse(input: ParseInput) {
  const status = getStatus();

  // Fast path: keep ctx/allocations out unless needed
  if (/* invalid condition */) {
    const ctx = this._getOrReturnCtx(input); // create only now
    addIssueToContext(ctx, { code: '...' });
    status.dirty();
    return status;
  }

  // No try/catch/throw in normal flow
  return status;
}

Before/after any performance-sensitive change, run benchmarks or flamegraphs on realistic workloads (include the operations that trigger JIT/fast-pass warmup if relevant).