{"record":{"id":"f5b0e2aeed66f833","repo":"denoland/deno","slug":"counter-can-only-be-incremented","errorCode":null,"errorMessage":"Counter can only be incremented","messagePattern":"Counter can only be incremented","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ext/telemetry/telemetry.ts","lineNumber":1078,"sourceCode":"        );\n        i += 1;\n      }\n    }\n  }\n}\n\nclass Counter {\n  #instrument: Instrument | null;\n  #upDown: boolean;\n\n  constructor(instrument: Instrument | null, upDown: boolean) {\n    this.#instrument = instrument;\n    this.#upDown = upDown;\n  }\n\n  add(value: number, attributes?: MetricAttributes, _context?: Context): void {\n    if (value < 0 && !this.#upDown) {\n      throw new Error(\"Counter can only be incremented\");\n    }\n    record(this.#instrument, value, attributes);\n  }\n}\n\nclass Gauge {\n  #instrument: Instrument | null;\n\n  constructor(instrument: Instrument | null) {\n    this.#instrument = instrument;\n  }\n\n  record(\n    value: number,\n    attributes?: MetricAttributes,\n    _context?: Context,\n  ): void {\n    record(this.#instrument, value, attributes);","sourceCodeStart":1060,"sourceCodeEnd":1096,"githubUrl":"https://github.com/denoland/deno/blob/a961cdec3b1948844414ebeecc697dcad76df2d1/ext/telemetry/telemetry.ts#L1060-L1096","documentation":"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().","triggerScenarios":"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.","commonSituations":"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.","solutions":["Create the instrument with meter.createUpDownCounter(name) when deltas can be negative","Clamp the value: counter.add(Math.max(0, value))","Restructure to monotonic events, e.g. count 'items_removed' in its own counter instead of subtracting"],"exampleFix":"// before\nconst size = meter.createCounter('queue.size');\nsize.add(onQueue.length - prevLength); // throws when queue shrinks\n\n// after\nconst size = meter.createUpDownCounter('queue.size');\nsize.add(onQueue.length - prevLength);","handlingStrategy":"validation","validationCode":"if (value >= 0 || upDown) {\n  counter.add(value, attributes);\n} else {\n  // log or route to an up-down counter\n}","typeGuard":"const isNonNegative = (n: number): boolean =>\n  typeof n === \"number\" && Number.isFinite(n) && n >= 0;","tryCatchPattern":"try {\n  counter.add(delta, attrs);\n} catch (e) {\n  if (e instanceof Error && e.message === \"Counter can only be incremented\") {\n    upDown.add(delta, attrs); // fallback instrument that accepts negatives\n  } else {\n    throw e;\n  }\n}","preventionTips":["Choose createUpDownCounter whenever deltas can be negative","Compute deltas with clamping when feeding monotonic counters","Unit-test instrumentation wrappers with negative inputs"],"tags":["opentelemetry","metrics","telemetry","counter","validation"],"backgroundTag":"negative-value-rejected","analyzedSha":"a961cdec3b1948844414ebeecc697dcad76df2d1","analyzedAt":"2026-08-20T13:07:44.778Z","contentChangedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}