santifer/career-ops · error · TypeError
createLockWaitPolicy: hardDeadline must be a number or…
Error message
createLockWaitPolicy: hardDeadline must be a number or omitted, got ${String(hardDeadline)} What it means
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'.
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
Example fix
// before
const policy = createLockWaitPolicy({ hardDeadline: process.env.DEADLINE_MS }); // string | undefined
// after
const raw = process.env.DEADLINE_MS;
const hardDeadline = raw === undefined ? undefined : Number(raw);
if (hardDeadline !== undefined && Number.isNaN(hardDeadline)) throw new Error(`invalid DEADLINE_MS: ${raw}`);
const policy = createLockWaitPolicy({ hardDeadline }); Defensive patterns
Strategy: validation
Validate before calling
const deadline = raw === undefined ? undefined : Number(raw); if (deadline !== undefined && (typeof deadline !== 'number' || Number.isNaN(deadline))) throw new Error(`bad hardDeadline: ${raw}`); Type guard
const isValidDeadline = (v) => v === undefined || (typeof v === 'number' && !Number.isNaN(v));
Try / catch
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; } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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).
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Report number must be a positive integer, got
- 4dayweek: invalid URL
- 4dayweek: URL must use HTTPS
- a 40-hex commit --sha is required
- apify: entry has invalid field_map. Each of title, url…
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/056aa0c7c26a8117.
Report an issue: GitHub.
Appendix: source
Thrown at pipeline-lock.mjs:273
export function createLockWaitPolicy(lockDir, { timeoutMs, retryMs, deadline, hardDeadline }) {
let perHolderDeadline = deadline;
let lastFingerprint;
// The ceiling is the policy's, not each caller's (#3895). Three lock modules
// used to write `Date.now() + timeoutMs * 10` at their own call sites, so the
// bound the clamp below applies existed in four places. Copies of an
// invariant stay correct only until one is edited, and a wrong ceiling fails
// silently — it changes retry timing, which nothing asserts and nobody
// reports. Deriving it here leaves one copy for them all.
const ceiling = hardDeadline ?? Date.now() + timeoutMs * DEFAULT_MAX_WAIT_FACTOR;
// A ceiling that is not a number is not a ceiling, and it fails INVISIBLY:
// Math.min(x, NaN) is NaN, Math.max(0, NaN) is NaN, and setTimeout(NaN) fires
// immediately — so a caller that got this wrong would spin hot through the
// retry loop rather than raise anything. Infinity is a deliberate, legible
// "no ceiling" (acquirePipelineLock passes it for maxWaitMs: Infinity) and is
// kept; NaN and non-numbers are mistakes and are refused where they are made.
if (typeof ceiling !== 'number' || Number.isNaN(ceiling)) {
throw new TypeError(
`createLockWaitPolicy: hardDeadline must be a number or omitted, got ${String(hardDeadline)}`,
);
}
// Jittered backoff, never sleeping past the ceiling. An uncapped sleep can
// cross the ceiling and let the NEXT mkdir succeed, returning a lock after
// the documented absolute limit — an overshoot of up to 1.5x retryMs. Waking
// exactly at the ceiling means the check at the top of the loop is what
// decides, rather than whichever of the two happened to be later.
const backoffMs = () => Math.max(0, Math.min(
retryMs * (0.5 + Math.random()),
ceiling - Date.now(),
));
// The per-holder deadline, evaluated the SAME way everywhere: an expired
// deadline only means "give up" when the lock has not changed hands since we
// last looked. Otherwise the window is re-armed and the caller waits again.
//View on GitHub (pinned to aac998c7ed)