denoland/deno · error · Error

Counter can only be incremented

Error message

Counter can only be incremented

What it means

Counter.add() in Deno's OTel shim (ext/telemetry/telemetry.ts:1056) throws when value is negative and the counter is a plain counter (created via createCounter, upDown = false). Counters are monotonic per the metrics specification; only up-down counters created with createUpDownCounter accept negative deltas. The check happens at call time, before record().

Source

Thrown at ext/telemetry/telemetry.ts:1078

        );
        i += 1;
      }
    }
  }
}

class Counter {
  #instrument: Instrument | null;
  #upDown: boolean;

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

  add(value: number, attributes?: MetricAttributes, _context?: Context): void {
    if (value < 0 && !this.#upDown) {
      throw new Error("Counter can only be incremented");
    }
    record(this.#instrument, value, attributes);
  }
}

class Gauge {
  #instrument: Instrument | null;

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

  record(
    value: number,
    attributes?: MetricAttributes,
    _context?: Context,
  ): void {
    record(this.#instrument, value, attributes);

View on GitHub (pinned to a961cdec3b)

Solutions

  1. Create the instrument with meter.createUpDownCounter(name) when deltas can be negative
  2. Clamp the value: counter.add(Math.max(0, value))
  3. Restructure to monotonic events, e.g. count 'items_removed' in its own counter instead of subtracting

Example fix

// before
const size = meter.createCounter('queue.size');
size.add(onQueue.length - prevLength); // throws when queue shrinks

// after
const size = meter.createUpDownCounter('queue.size');
size.add(onQueue.length - prevLength);
Defensive patterns

Strategy: validation

Validate before calling

if (value >= 0 || upDown) {
  counter.add(value, attributes);
} else {
  // log or route to an up-down counter
}

Type guard

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

Try / catch

try {
  counter.add(delta, attrs);
} catch (e) {
  if (e instanceof Error && e.message === "Counter can only be incremented") {
    upDown.add(delta, attrs); // fallback instrument that accepts negatives
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: counter.add(-1) to 'undo' a previous increment; counter.add(delta) where delta = removed - added from a diff computation and can go negative; generic instrumentation wrappers that pass through arbitrary signed values to a plain counter.

Common situations: Tracking queue size or connection count with createCounter and then feeding signed deltas on decrement; applying metrics patches/diffs from external collectors; math errors that underflow into negatives.

Related errors


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