{"record":{"id":"fa4f331cb85760c4","repo":"denoland/deno","slug":"observable-counters-can-only-be-incremented","errorCode":null,"errorMessage":"Observable counters can only be incremented","messagePattern":"Observable counters can only be incremented","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ext/telemetry/telemetry.ts","lineNumber":1163,"sourceCode":"}\n\nclass ObservableResult {\n  #instrument: Instrument | null;\n  #isRegularCounter: boolean;\n\n  constructor(instrument: Instrument | null, isRegularCounter: boolean) {\n    this.#instrument = instrument;\n    this.#isRegularCounter = isRegularCounter;\n  }\n\n  observe(\n    this: ObservableResult,\n    value: number,\n    attributes?: MetricAttributes,\n  ): void {\n    if (this.#isRegularCounter) {\n      if (value < 0) {\n        throw new Error(\"Observable counters can only be incremented\");\n      }\n    }\n    recordObservable(this.#instrument, value, attributes);\n  }\n}\n\nasync function observe(): Promise<void> {\n  if (ISOLATE_METRICS) {\n    op_otel_collect_isolate_metrics();\n  }\n\n  const promises: Promise<void>[] = [];\n  // Primordials are not needed, because this is a SafeMap.\n  // deno-lint-ignore deno-internal/prefer-primordials\n  for (const { 0: observable, 1: callbacks } of INDIVIDUAL_CALLBACKS) {\n    const result = getObservableResult(observable);\n    // Primordials are not needed, because this is a SafeSet.\n    // deno-lint-ignore deno-internal/prefer-primordials","sourceCodeStart":1145,"sourceCodeEnd":1181,"githubUrl":"https://github.com/denoland/deno/blob/a961cdec3b1948844414ebeecc697dcad76df2d1/ext/telemetry/telemetry.ts#L1145-L1181","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use meter.createObservableUpDownCounter(name) if observations can be negative","Observe absolute cumulative totals (monotonic counters) instead of deltas","Clamp in the callback: result.observe(Math.max(0, value))"],"exampleFix":"// before\nmeter.createObservableCounter('jobs_done', (res) => {\n  res.observe(jobsNow - jobsPrev); // negative when counter reset\n});\n\n// after\nmeter.createObservableCounter('jobs_done', (res) => {\n  res.observe(jobsNow); // absolute cumulative value\n});","handlingStrategy":"validation","validationCode":"// inside the observable callback\nif (value >= 0 || !isMonotonicInstrument) {\n  result.observe(value, attributes);\n}","typeGuard":"const isObserveable = (n: number): boolean =>\n  typeof n === \"number\" && Number.isFinite(n);","tryCatchPattern":"try {\n  result.observe(value);\n} catch (e) {\n  if (\n    e instanceof Error &&\n    e.message === \"Observable counters can only be incremented\"\n  ) {\n    result.observe(0); // or skip this collection round\n  } else {\n    throw e;\n  }\n}","preventionTips":["Observe absolute totals on observable counters, never deltas","Use createObservableUpDownCounter for signed point-in-time metrics","Guard collector callbacks against counter resets (previous > current)"],"tags":["opentelemetry","metrics","telemetry","observable-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"}