{"record":{"id":"5cfaf07b765b5a31","repo":"mozilla/pdf.js","slug":"invalid-updatescale-options-either-steps-or-sc","errorCode":null,"errorMessage":"Invalid updateScale options: either `steps` or `scaleFactor` must be provided.","messagePattern":"Invalid updateScale options: either `steps` or `scaleFactor` must be provided\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/pdf_viewer.js","lineNumber":2585,"sourceCode":"   * @property {number} [steps]\n   * @property {Array} [origin] x and y coordinates of the scale\n   *                            transformation origin.\n   * @property {Array<number>} [pan] - Horizontal and vertical gesture deltas.\n   */\n\n  /**\n   * Changes the current zoom level by the specified amount.\n   * @param {ChangeScaleOptions} [options]\n   */\n  updateScale({\n    drawingDelay,\n    scaleFactor = null,\n    steps = null,\n    origin,\n    pan = null,\n  }) {\n    if (steps === null && scaleFactor === null) {\n      throw new Error(\n        \"Invalid updateScale options: either `steps` or `scaleFactor` must be provided.\"\n      );\n    }\n    if (!this.pdfDocument) {\n      return;\n    }\n    let newScale = this._currentScale;\n    if (scaleFactor > 0 && scaleFactor !== 1) {\n      newScale = Math.round(newScale * scaleFactor * 100) / 100;\n    } else if (steps) {\n      const delta = steps > 0 ? DEFAULT_SCALE_DELTA : 1 / DEFAULT_SCALE_DELTA;\n      const round = steps > 0 ? Math.ceil : Math.floor;\n      steps = Math.abs(steps);\n      do {\n        newScale = round((newScale * delta).toFixed(2) * 10) / 10;\n      } while (--steps > 0);\n    }\n    newScale = MathClamp(newScale, MIN_SCALE, MAX_SCALE);","sourceCodeStart":2567,"sourceCodeEnd":2603,"githubUrl":"https://github.com/mozilla/pdf.js/blob/5903d58d58e4dd9ce6ffa3834aea8480f06b4ada/web/pdf_viewer.js#L2567-L2603","documentation":"PDFViewer.updateScale() changes the current zoom either by a multiplicative `scaleFactor` or by N discrete `steps` (powered by DEFAULT_SCALE_DELTA). Both parameters default to `null`, so the method refuses to guess and throws when neither is supplied. It is a contract guard: exactly one zoom strategy must be present. Note that `increaseScale`/`decreaseScale` always inject `steps`, so this only fires when `updateScale` is called directly with an empty or stripped options object.","triggerScenarios":"Calling `pdfViewer.updateScale({})`, `pdfViewer.updateScale({ origin, pan })` (only transform metadata, no zoom delta), or spreading a user-supplied options bag whose `steps`/`scaleFactor` keys are absent/undefined. Also triggered by code that destructures options and forwards a filtered subset that accidentally drops both keys.","commonSituations":"A custom toolbar or pinch-zoom handler builds a ChangeScaleOptions object dynamically and forwards it without guaranteeing a zoom field; refactoring that removes the `scaleFactor` branch but forgets to set `steps`; passing `scaleFactor: 0` or `scaleFactor: 1` is NOT the trigger (those fall through to the `steps` branch) — the trigger is strictly both being null.","solutions":["Always pass exactly one of `steps` (integer, negative shrinks) or `scaleFactor` (positive multiplier != 1) when calling updateScale.","If you only have a target scale, compute it yourself: prefer `pdfViewer.currentScaleValue` / the `#setScale` path, or derive `scaleFactor = targetScale / pdfViewer.currentScale`.","For incremental zoom buttons, call `increaseScale()` / `decreaseScale()` instead of `updateScale` directly — they inject `steps` for you.","When forwarding a user/options bag, default it explicitly: `updateScale({ steps: 1, ...opts })` so a missing key cannot leave both null."],"exampleFix":"// before\npdfViewer.updateScale({ origin: [x, y] });\n// after\npdfViewer.updateScale({ scaleFactor: 1.1, origin: [x, y] });\n// or, for discrete stepping:\npdfViewer.increaseScale({ origin: [x, y] });","handlingStrategy":"validation","validationCode":"// Validate before calling updateScale.\nfunction safeUpdateScale(viewer, opts = {}) {\n  const hasFactor = typeof opts.scaleFactor === 'number' && opts.scaleFactor > 0 && opts.scaleFactor !== 1;\n  const hasSteps = Number.isInteger(opts.steps);\n  if (!hasFactor && !hasSteps) {\n    throw new TypeError('updateScale needs `steps` (integer) or `scaleFactor` (>0, !=1)');\n  }\n  viewer.updateScale(opts);\n}","typeGuard":"/** @param {unknown} o */\nfunction isChangeScaleOptions(o) {\n  if (!o || typeof o !== 'object') return false;\n  const { scaleFactor, steps } = /** @type {any} */ (o);\n  const factorOk = scaleFactor == null || (typeof scaleFactor === 'number' && scaleFactor > 0);\n  const stepsOk = steps == null || Number.isInteger(steps);\n  return factorOk && stepsOk && (scaleFactor != null || steps != null);\n}","tryCatchPattern":"try {\n  viewer.updateScale(opts);\n} catch (e) {\n  if (/Invalid updateScale options/.test(e.message)) {\n    // degrade: default to one discrete step instead of crashing the UI\n    viewer.increaseScale({ origin: opts.origin });\n  } else {\n    throw e;\n  }\n}","preventionTips":["Prefer increaseScale()/decreaseScale() wrappers — they always set steps.","When forwarding a user options bag, merge a default: `{ steps: 1, ...userOpts }`.","Treat scaleFactor of 0, negative, or 1 as 'no zoom intent' and convert to steps."],"tags":["zoom","api-misuse","validation","pdf-viewer"],"backgroundTag":null,"analyzedSha":"5903d58d58e4dd9ce6ffa3834aea8480f06b4ada","analyzedAt":"2026-08-13T02:28:27.364Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}