Skip to content

Clock

Every notion of “now” and every delay in Effect flows through the Clock service. Schedules, Effect.sleep, Effect.timeout, and retries all ask the Clock what time it is and to wait — none of them touch Date.now or setTimeout directly. That single indirection is what makes time in Effect deterministic and testable: swap the live clock for a TestClock and you control the passage of time exactly.

import { Clock, Effect } from "effect"
// Measure how long a piece of work takes, reading time from the Clock service.
const timed = Effect.gen(function* () {
const start = yield* Clock.currentTimeMillis
yield* doWork
const end = yield* Clock.currentTimeMillis
yield* Effect.log(`work took ${end - start}ms`)
})
declare const doWork: Effect.Effect<void>

Clock.currentTimeMillis and Clock.currentTimeNanos are effects that yield the current time at millisecond or nanosecond precision. Reaching for Date.now() would read the wall clock directly, bypassing the runtime and making the code untestable — always read time through Clock.

Effect.sleep suspends the current fiber for a duration. It is interruptible and implemented on top of the Clock, so it cooperates with concurrency and with the TestClock:

import { Effect } from "effect"
const program = Effect.gen(function* () {
yield* Effect.log("starting")
yield* Effect.sleep("2 seconds") // accepts any Duration.Input
yield* Effect.log("two seconds later")
})

Unlike a blocking setTimeout, a sleeping fiber consumes no thread and can be interrupted — if the surrounding scope is cancelled mid-sleep, the wait ends immediately. See Concurrency for how fibers and interruption fit together.

The accessors above cover most needs. When you want the whole service — for example to call the synchronous currentTimeMillisUnsafe() inside a tight, already-effectful loop — read the Clock reference or use Clock.clockWith:

import { Clock, Effect } from "effect"
const program = Effect.gen(function* () {
const clock = yield* Clock.Clock
// Synchronous read; only valid because we already hold the service.
return clock.currentTimeMillisUnsafe()
})

The real value of routing time through a service shows up in tests. With @effect/vitest, it.effect runs your effect against a TestClock whose time only advances when you tell it to. TestClock.adjust moves the clock forward and runs anything scheduled to fire on or before the new time — so a five-second sleep “completes” the instant you advance the clock five seconds, with no real waiting.

import { assert, describe, it } from "@effect/vitest"
import { Effect, Ref } from "effect"
import { TestClock } from "effect/testing"
describe("polling", () => {
it.effect("ticks once per interval", () =>
Effect.gen(function* () {
const ticks = yield* Ref.make(0)
// Fork a worker that increments a counter every second, forever.
yield* Ref.update(ticks, (n) => n + 1).pipe(
Effect.delay("1 second"),
Effect.forever,
Effect.forkChild
)
// No real time passes — we drive the clock by hand.
yield* TestClock.adjust("1 second")
assert.strictEqual(yield* Ref.get(ticks), 1)
yield* TestClock.adjust("3 seconds")
assert.strictEqual(yield* Ref.get(ticks), 4)
}))
})

The same technique verifies retry backoff, cron jobs, and timeouts in microseconds rather than real wall-clock time. To advance to an absolute moment instead of by a delta, use TestClock.setTime(epochMillis).

Time, like every other capability in Effect, is just a service — which is what lets schedules and retries be tested as easily as pure functions. For more on the testing tools, see Testing.