{"record":{"id":"056aa0c7c26a8117","repo":"santifer/career-ops","slug":"createlockwaitpolicy-harddeadline-must-be-a-number-or","errorCode":null,"errorMessage":"createLockWaitPolicy: hardDeadline must be a number or omitted, got ${String(hardDeadline)}","messagePattern":"createLockWaitPolicy: hardDeadline must be a number or omitted, got (.+?)","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"pipeline-lock.mjs","lineNumber":273,"sourceCode":"export function createLockWaitPolicy(lockDir, { timeoutMs, retryMs, deadline, hardDeadline }) {\n  let perHolderDeadline = deadline;\n  let lastFingerprint;\n\n  // The ceiling is the policy's, not each caller's (#3895). Three lock modules\n  // used to write `Date.now() + timeoutMs * 10` at their own call sites, so the\n  // bound the clamp below applies existed in four places. Copies of an\n  // invariant stay correct only until one is edited, and a wrong ceiling fails\n  // silently — it changes retry timing, which nothing asserts and nobody\n  // reports. Deriving it here leaves one copy for them all.\n  const ceiling = hardDeadline ?? Date.now() + timeoutMs * DEFAULT_MAX_WAIT_FACTOR;\n  // A ceiling that is not a number is not a ceiling, and it fails INVISIBLY:\n  // Math.min(x, NaN) is NaN, Math.max(0, NaN) is NaN, and setTimeout(NaN) fires\n  // immediately — so a caller that got this wrong would spin hot through the\n  // retry loop rather than raise anything. Infinity is a deliberate, legible\n  // \"no ceiling\" (acquirePipelineLock passes it for maxWaitMs: Infinity) and is\n  // kept; NaN and non-numbers are mistakes and are refused where they are made.\n  if (typeof ceiling !== 'number' || Number.isNaN(ceiling)) {\n    throw new TypeError(\n      `createLockWaitPolicy: hardDeadline must be a number or omitted, got ${String(hardDeadline)}`,\n    );\n  }\n\n  // Jittered backoff, never sleeping past the ceiling. An uncapped sleep can\n  // cross the ceiling and let the NEXT mkdir succeed, returning a lock after\n  // the documented absolute limit — an overshoot of up to 1.5x retryMs. Waking\n  // exactly at the ceiling means the check at the top of the loop is what\n  // decides, rather than whichever of the two happened to be later.\n  const backoffMs = () => Math.max(0, Math.min(\n    retryMs * (0.5 + Math.random()),\n    ceiling - Date.now(),\n  ));\n\n  // The per-holder deadline, evaluated the SAME way everywhere: an expired\n  // deadline only means \"give up\" when the lock has not changed hands since we\n  // last looked. Otherwise the window is re-armed and the caller waits again.\n  //","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/pipeline-lock.mjs#L255-L291","documentation":"createLockWaitPolicy validates its ceiling (derived from hardDeadline/maxWaitMs) before using it in backoff sleeps. Because Math.min/max with NaN produce NaN and setTimeout(NaN) fires immediately, a bad value would cause a hot retry spin instead of an error — so NaN and non-numbers are refused eagerly with a TypeError at the call site. Infinity is allowed as an explicit 'no ceiling'.","triggerScenarios":"createLockWaitPolicy({ hardDeadline: <non-number> }) — e.g. a string from CLI parsing, null from a failed lookup, NaN from an arithmetic bug — or a computed ceiling that evaluates to NaN.","commonSituations":"Passing process.argv string '5000' instead of a number; a Date arithmetic bug producing NaN; config value loaded as null/undefined then computed; forgetting to omit hardDeadline and passing undefined-with-units confusion (e.g. 'Infinity' string).","solutions":["Log/check the hardDeadline value you passed — coerce with Number() or parse with parseInt before the call","Pass a finite positive number, Infinity, or omit the option entirely","Fix the upstream computation producing NaN (often a failed parseInt or Date math)","Wrap the call in a validation that asserts typeof hardDeadline === 'number' || hardDeadline === undefined"],"exampleFix":"// before\nconst policy = createLockWaitPolicy({ hardDeadline: process.env.DEADLINE_MS }); // string | undefined\n// after\nconst raw = process.env.DEADLINE_MS;\nconst hardDeadline = raw === undefined ? undefined : Number(raw);\nif (hardDeadline !== undefined && Number.isNaN(hardDeadline)) throw new Error(`invalid DEADLINE_MS: ${raw}`);\nconst policy = createLockWaitPolicy({ hardDeadline });","handlingStrategy":"validation","validationCode":"const deadline = raw === undefined ? undefined : Number(raw); if (deadline !== undefined && (typeof deadline !== 'number' || Number.isNaN(deadline))) throw new Error(`bad hardDeadline: ${raw}`);","typeGuard":"const isValidDeadline = (v) => v === undefined || (typeof v === 'number' && !Number.isNaN(v));","tryCatchPattern":"try { policy = createLockWaitPolicy({ hardDeadline }); } catch (e) { if (e instanceof TypeError && e.message.includes('hardDeadline')) { console.error('Pass a number, Infinity, or omit hardDeadline:', e.message); } else throw e; }","preventionTips":["Coerce CLI/env strings with Number() before passing","Never compute deadlines with potentially-NaN arithmetic without checking Number.isNaN first","Use Infinity explicitly for 'no ceiling' rather than a sentinel value","Add an assertion upstream where the deadline value originates"],"tags":["validation","lock","nan","typeerror"],"backgroundTag":"invalid-argument-value","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}