denoland/deno · error · Error

Observable counters can only be incremented

Error message

Observable counters can only be incremented

What it means

ObservableResult.observe() (ext/telemetry/telemetry.ts:1141) throws when it observes a negative value on an instrument created with createObservableCounter (ObservableResult constructed with isRegularCounter = true). Observable counters are monotonic; observable up-down counters and observable gauges pass isRegularCounter = false and accept negative values.

Solutions

  1. Use meter.createObservableUpDownCounter(name) if observations can be negative
  2. Observe absolute cumulative totals (monotonic counters) instead of deltas
  3. Clamp in the callback: result.observe(Math.max(0, value))

Example fix

// before
meter.createObservableCounter('jobs_done', (res) => {
  res.observe(jobsNow - jobsPrev); // negative when counter reset
});

// after
meter.createObservableCounter('jobs_done', (res) => {
  res.observe(jobsNow); // absolute cumulative value
});
Defensive patterns

Strategy: validation

Validate before calling

// inside the observable callback
if (value >= 0 || !isMonotonicInstrument) {
  result.observe(value, attributes);
}

Type guard

const isObserveable = (n: number): boolean =>
  typeof n === "number" && Number.isFinite(n);

Try / catch

try {
  result.observe(value);
} catch (e) {
  if (
    e instanceof Error &&
    e.message === "Observable counters can only be incremented"
  ) {
    result.observe(0); // or skip this collection round
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Inside a createObservableCounter callback, calling result.observe(current - previous) where the underlying metric decreased between collections; reporting a signed value through an observable counter because it was the first observable factory reached for.

Common situations: Switching an instrument from createObservableGauge to createObservableCounter without adjusting the callback to report absolute totals; delta-style collectors feeding cumulative instruments; metrics that dip below zero during resets or counter restarts.

Related errors


AI-assisted analysis of denoland/deno@a961cdec3b (2026-08-20). Data as JSON: /api/errors/fa4f331cb85760c4. Report an issue: GitHub.

Appendix: source

Thrown at ext/telemetry/telemetry.ts:1163

}

class ObservableResult {
  #instrument: Instrument | null;
  #isRegularCounter: boolean;

  constructor(instrument: Instrument | null, isRegularCounter: boolean) {
    this.#instrument = instrument;
    this.#isRegularCounter = isRegularCounter;
  }

  observe(
    this: ObservableResult,
    value: number,
    attributes?: MetricAttributes,
  ): void {
    if (this.#isRegularCounter) {
      if (value < 0) {
        throw new Error("Observable counters can only be incremented");
      }
    }
    recordObservable(this.#instrument, value, attributes);
  }
}

async function observe(): Promise<void> {
  if (ISOLATE_METRICS) {
    op_otel_collect_isolate_metrics();
  }

  const promises: Promise<void>[] = [];
  // Primordials are not needed, because this is a SafeMap.
  // deno-lint-ignore deno-internal/prefer-primordials
  for (const { 0: observable, 1: callbacks } of INDIVIDUAL_CALLBACKS) {
    const result = getObservableResult(observable);
    // Primordials are not needed, because this is a SafeSet.
    // deno-lint-ignore deno-internal/prefer-primordials

View on GitHub (pinned to a961cdec3b)