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.

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)

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.