A time-dependent rule can look like an ordinary comparison. A link is valid until a timestamp. A reminder becomes eligible after a delay. A report includes records created before a cutoff.
The difficulty appears when “now” is read from the environment inside the rule. Two calls with identical visible inputs can produce different answers. A test may pass in the morning and fail later without any change to the code.
Making time an explicit dependency gives that hidden input a name. It also forces a useful design question: which kind of time does this decision need?
Start with the smallest boundary
Consider a temporary download link. Its expiration timestamp is stored as milliseconds since the Unix epoch. The rule says the link is valid strictly before that timestamp.
A first implementation might read the system clock directly:
function isLinkValid(expiresAtMs: number): boolean {
return Date.now() < expiresAtMs
}
The comparison is easy to read, but its input list is incomplete. Expiration is supplied by the caller; the current instant arrives through a global function.
For a small rule, passing the instant is enough:
function isLinkValid(
expiresAtMs: number,
nowMs: number
): boolean {
return nowMs < expiresAtMs
}
The application boundary can call this with Date.now(). Tests can call it with a fixed value. No clock class or dependency injection framework is required.
MDN documents Date.now() as an epoch-based timestamp in milliseconds. Naming the units in both parameters makes the comparison’s contract visible.
Validate external values before they reach this function. An invalid timestamp is a separate input problem, not a reason to complicate the time source.
Test the instant where behavior changes
Suppose a link expires at 1_000 in a simplified test fixture. Three cases describe the boundary:
isLinkValid(1_000, 999) // true: before expiration
isLinkValid(1_000, 1_000) // false: at expiration
isLinkValid(1_000, 1_001) // false: after expiration
These cases make an otherwise easy-to-miss decision explicit. Is expiration inclusive? The chosen rule answers that question without depending on how quickly a test runner executes.
Waiting for real time to pass would add delay while giving less control over the exact instant being tested. Fixed inputs let the test examine the policy directly.
Use the same approach for eligibility windows, cooldowns, and age-based filters. Choose values immediately before, exactly at, and immediately after the threshold. The important case is often equality, where an innocent-looking change from < to <= changes the product’s promise.
Read once for one logical decision
Passing time explicitly also raises a second question: how often should the application read it?
Imagine assembling a list of links for one response. If each row reads the clock independently, two rows with the same expiration can receive different statuses when processing crosses the boundary.
If the response should represent one snapshot, capture one instant:
const evaluatedAtMs = Date.now()
const result = links.map((link) => ({
id: link.id,
valid: isLinkValid(link.expiresAtMs, evaluatedAtMs)
}))
This is a consistency decision. It does not guarantee that a link remains valid when someone uses it later. The download endpoint must evaluate validity again when enforcing access.
A snapshot is useful for presentation; a fresh check is necessary for a later operation. Naming both decisions prevents one timestamp from quietly serving incompatible purposes.
Use a clock when behavior spans several steps
A larger service may need the current instant in several operations. Passing a number through every layer can become awkward. A small clock interface can then make the dependency easier to carry:
interface Clock {
nowMs(): number
}
const systemClock: Clock = {
nowMs: () => Date.now()
}
A test clock can hold a value and expose an explicit advance operation. The service can observe a fixed instant, then a later instant, without sleeping.
Keep this interface narrow. If the service only compares timestamps, it needs a way to read time. If it also schedules callbacks, that is another dependency. Advancing a custom clock does not automatically run callbacks registered with real timers.
Separating reading time from scheduling work makes tests clearer. One test can verify eligibility; another can verify that the scheduler invokes the operation under the intended conditions.
Separate timestamps from elapsed durations
Not every time question asks for a date.
“When does this link expire?” needs an instant that can be stored and compared later. “How long did this operation take?” needs elapsed time within an execution context.
For short browser measurements, a monotonic clock avoids changes caused by adjusting the system’s calendar clock:
const startedAt = performance.now()
await loadPreview()
const elapsedMs = performance.now() - startedAt
MDN’s performance.now() reference explains its monotonic time basis and its difference from Date.now(). It also documents platform differences during system sleep, which matter for long measurements.
Keep the two representations separate. A duration reading is not a portable expiration timestamp. An epoch timestamp is not automatically the best tool for measuring a short operation.
Keep calendar policy visible
A business rule such as “available until the end of the day” needs more than a clock. It needs a definition of the day.
Specify the time zone, the cutoff, and what happens at the boundary. If the rule follows a local calendar, avoid silently replacing it with an elapsed duration. “Tomorrow at the same local time” and “after a fixed number of hours” express different policies.
Once that policy has produced an expiration instant, the comparison can stay small. Calendar interpretation belongs where the business rule is defined; the validity check should not rediscover it.
An injectable clock also does not make different machines agree about time. For decisions that must be consistent across services, choose which service or datastore owns the authoritative check.
Make the next time rule easier to explain
Start with one function that calls Date.now() internally. Ask whether it decides a business rule, captures a snapshot, or measures elapsed work.
Move the time reading to a clear boundary. Name the units. Add the equality case. Introduce a clock interface only when several operations need it.
The result is a rule whose inputs can be read, discussed, and tested. Time still changes. The behavior no longer has to be mysterious.