Skip to content

DateTime and Duration

Two complementary types model time in Effect. A Duration is a span — “5 seconds”, “2 hours” — used for timeouts, retries, and scheduling. A DateTime is an absolute instant on the timeline, optionally carrying a time zone. They work together: subtracting two DateTimes gives a Duration, and adding a Duration to a DateTime produces a new instant.

Crucially, you never read the wall clock directly. Instead of Date.now(), obtain the current time from the Clock service via DateTime.now. This keeps your code deterministic and testable — tests can supply a fixed clock (see TestClock).

import { DateTime, Duration, Effect } from "effect"
const program = Effect.gen(function* () {
// Read the current instant from the Clock service (not Date.now)
const now = yield* DateTime.now
// Add a span of time to get a future instant
const inOneHour = DateTime.add(now, { hours: 1 })
// The gap between two instants is a Duration
const gap = DateTime.distance(now, inOneHour)
console.log(DateTime.formatIso(inOneHour))
console.log(Duration.format(gap)) // "1h"
})
Effect.runPromise(program)

Both types are immutable and compare by value: two equal instants are Equal.equals, and so are two equal durations.

A Duration stores a span as either an integer milliseconds number, an integer nanoseconds bigint, or one of the infinity sentinels. It can be finite or infinite (positive or negative). Constructors pick the representation for you, and arithmetic preserves it.

Each constructor takes a number in the named unit and returns a Duration:

import { Duration } from "effect"
const d1 = Duration.seconds(30)
const d2 = Duration.minutes(5)
const d3 = Duration.hours(2)
const d4 = Duration.millis(1500)
// A Duration also has well-known constants
const forever = Duration.infinity
const none = Duration.zero

Many Effect APIs (delays, timeouts, schedules) accept a Duration.Input directly, so you rarely call a constructor at the call site. An input can be:

  • a number — interpreted as milliseconds (not seconds!),
  • a bigint — interpreted as nanoseconds,
  • a [seconds, nanos] tuple — Node-style high-resolution time,
  • a string like "2 seconds" / "500 millis" / "Infinity" / "-Infinity",
  • a DurationObject such as { hours: 1, minutes: 30 },
  • or an existing Duration.
import { Effect } from "effect"
// "2 seconds" is a valid Duration.Input
const delayed = Effect.succeed(1).pipe(Effect.delay("2 seconds"))

The math and comparison helpers are dual (data-first and data-last), so they work both directly and inside pipe:

import { Duration } from "effect"
// Add two spans together
console.log(Duration.format(Duration.sum(Duration.minutes(1), Duration.seconds(30))))
// "1m 30s"
// Scale a span
console.log(Duration.format(Duration.times(Duration.seconds(10), 3)))
// "30s"
// Convert to a primitive at the boundary
console.log(Duration.toMillis(Duration.seconds(2))) // 2000
// Range check via the provided Order
console.log(
Duration.between(Duration.seconds(5), {
minimum: Duration.seconds(1),
maximum: Duration.seconds(10)
})
) // true

Duration.format renders a human-readable string, which is handy in logs. For ordering, Duration.Order is an Order<Duration> you can pass to sorting and comparison utilities.

A DateTime is always an absolute instant (epoch milliseconds) and is one of two variants:

  • DateTime.Utc — an instant with no associated zone. DateTime.now returns a Utc.
  • DateTime.Zoned — the same instant plus a TimeZone, used for wall-clock parts, formatting, and calendar-aware arithmetic.

A TimeZone is either TimeZone.Named (an IANA zone like "Europe/Rome") or TimeZone.Offset (a fixed offset in milliseconds from UTC). Equality and ordering use the instant, so two equal instants in different zones are still equivalent.

When a wall-clock time is ambiguous — during a DST gap or overlap — a Disambiguation ("compatible" | "earlier" | "later" | "reject") decides which instant is chosen.

DateTime.add / DateTime.subtract shift an instant by calendar parts (days, months, years are time-zone aware) or fixed parts (hours, minutes), returning a new instant:

import { DateTime, Effect } from "effect"
const program = Effect.gen(function* () {
const now = yield* DateTime.now
const tomorrow = DateTime.add(now, { days: 1 })
const lastWeek = DateTime.subtract(now, { weeks: 1 })
// distance is signed and returns a Duration
const span = DateTime.distance(lastWeek, tomorrow)
console.log(DateTime.formatIso(tomorrow))
})
Effect.runPromise(program)

For pure elapsed-time math (always milliseconds, never calendar-aware) use addDuration / subtractDuration with a Duration.Input.

Attach a zone with DateTime.setZoneNamed, which returns an Option because the zone name might be invalid:

import { DateTime, Effect, Option } from "effect"
const program = Effect.gen(function* () {
const now = yield* DateTime.now
const zoned = DateTime.setZoneNamed(now, "America/New_York")
Option.match(zoned, {
onNone: () => console.log("unknown time zone"),
onSome: (dt) => console.log(DateTime.formatIsoZoned(dt))
})
})
Effect.runPromise(program)

By default attaching a zone keeps the instant fixed (only the display changes). Pass { adjustForTimeZone: true } to instead interpret the input as wall-clock time in the target zone.

For workflows that operate “in the app’s zone”, provide a CurrentTimeZone service and use the *Current* helpers instead of threading a zone everywhere:

import { DateTime, Effect } from "effect"
const program = Effect.gen(function* () {
// reads the current Clock instant, in the provided zone
const now = yield* DateTime.nowInCurrentZone
console.log(DateTime.formatIsoZoned(now))
}).pipe(DateTime.withCurrentZoneNamed("Europe/London"))
Effect.runPromise(program)
  • DateTime.formatIso — UTC ISO 8601 string.
  • DateTime.format — locale-aware via Intl.DateTimeFormat options.
  • DateTime.formatIsoZonedYYYY-MM-DDTHH:mm:ss.sss+HH:MM[Time/Zone].
  • DateTime.toDateUtc — convert to a native Date at the boundary of your code.

Decodes a Duration.Input into a Duration safely, returning Option.none() on invalid input.

import { Duration, Option } from "effect"
Duration.fromInput("90 seconds").pipe(Option.map(Duration.toSeconds))
// => Some(90)
Duration.fromInput("nonsense" as any)
// => None

Like fromInput but throws on invalid input.

import { Duration } from "effect"
Duration.fromInputUnsafe([2, 500_000_000]) // 2s + 500ms
// => Duration "2s 500ms"
Duration.fromInputUnsafe({ days: 1 })
// => Duration "1d"

A Duration representing no time.

import { Duration } from "effect"
Duration.toMillis(Duration.zero) // => 0

Sentinels for unbounded spans.

import { Duration } from "effect"
Duration.toMillis(Duration.infinity) // => Infinity
Duration.toMillis(Duration.negativeInfinity) // => -Infinity

Build a finite duration from a bigint of nanoseconds or microseconds.

import { Duration } from "effect"
Duration.toMillis(Duration.nanos(BigInt(500_000_000))) // => 500
Duration.toMillis(Duration.micros(BigInt(500_000))) // => 500

millis / seconds / minutes / hours / days / weeks

Section titled “millis / seconds / minutes / hours / days / weeks”

Build a finite duration from a number in the named unit.

import { Duration } from "effect"
Duration.toMillis(Duration.millis(1000)) // => 1000
Duration.toMillis(Duration.seconds(30)) // => 30000
Duration.toMillis(Duration.minutes(5)) // => 300000
Duration.toMillis(Duration.hours(2)) // => 7200000
Duration.toMillis(Duration.days(1)) // => 86400000
Duration.toMillis(Duration.weeks(1)) // => 604800000

Type guard for Duration values.

import { Duration } from "effect"
Duration.isDuration(Duration.seconds(1)) // => true
Duration.isDuration(1000) // => false

true for any non-infinite duration.

import { Duration } from "effect"
Duration.isFinite(Duration.seconds(5)) // => true
Duration.isFinite(Duration.infinity) // => false

true if the duration is exactly zero.

import { Duration } from "effect"
Duration.isZero(Duration.zero) // => true
Duration.isZero(Duration.seconds(1)) // => false

Strict sign checks (zero is neither).

import { Duration } from "effect"
Duration.isNegative(Duration.seconds(-5)) // => true
Duration.isNegative(Duration.zero) // => false
Duration.isPositive(Duration.seconds(5)) // => true
Duration.isPositive(Duration.infinity) // => true

toMillis / toSeconds / toMinutes / toHours / toDays / toWeeks

Section titled “toMillis / toSeconds / toMinutes / toHours / toDays / toWeeks”

Convert to a number in the requested unit. These accept a Duration.Input, so you can pass a string directly.

import { Duration } from "effect"
Duration.toMillis(Duration.seconds(5)) // => 5000
Duration.toSeconds(Duration.minutes(2)) // => 120
Duration.toMinutes(Duration.hours(1)) // => 60
Duration.toHours(Duration.days(1)) // => 24
Duration.toDays(Duration.weeks(1)) // => 7
Duration.toWeeks(Duration.days(14)) // => 2

Get nanoseconds as Option<bigint>None for infinite durations.

import { Duration } from "effect"
Duration.toNanos(Duration.seconds(1)) // => Some(1000000000n)
Duration.toNanos(Duration.infinity) // => None

Get nanoseconds as a bigint, throwing for infinite durations.

import { Duration } from "effect"
Duration.toNanosUnsafe(Duration.seconds(2)) // => 2000000000n
// Duration.toNanosUnsafe(Duration.infinity) // throws

Convert to Node-style [seconds, nanoseconds] high-resolution time.

import { Duration } from "effect"
Duration.toHrTime(Duration.millis(1500)) // => [1, 500000000]

Decompose a finite duration into normalized signed components.

import { Duration } from "effect"
Duration.parts(Duration.sum(Duration.hours(25), Duration.minutes(90)))
// => { days: 1, hours: 2, minutes: 30, seconds: 0, millis: 0, nanos: 0 }

Render a human-readable string (used by the examples above).

import { Duration } from "effect"
Duration.format(Duration.millis(1000)) // => "1s"
Duration.format(Duration.millis(1001)) // => "1s 1ms"
Duration.format(Duration.infinity) // => "Infinity"

All of these are dual (data-first / data-last).

Add or subtract two durations. The result of subtract can be negative. Infinity follows IEEE-like rules (e.g. infinity - infinity = zero).

import { Duration } from "effect"
Duration.toSeconds(Duration.sum(Duration.seconds(5), Duration.seconds(3))) // => 8
Duration.toSeconds(Duration.subtract(Duration.seconds(10), Duration.seconds(3))) // => 7

Multiply a duration by a number.

import { Duration } from "effect"
Duration.toSeconds(Duration.times(Duration.seconds(5), 2)) // => 10

Safely divide by a finite, non-zero number; returns Option.

import { Duration, Option } from "effect"
Duration.divide(Duration.seconds(10), 2).pipe(Option.map(Duration.toSeconds))
// => Some(5)
Duration.divide(Duration.seconds(10), 0) // => None

Divide using fallback rules instead of Option (e.g. divide-by-zero of a finite duration yields signed infinity).

import { Duration } from "effect"
Duration.toSeconds(Duration.divideUnsafe(Duration.seconds(10), 2)) // => 5
Duration.toMillis(Duration.divideUnsafe(Duration.seconds(10), 0)) // => Infinity

Flip the sign, or take the absolute value.

import { Duration } from "effect"
Duration.toMillis(Duration.negate(Duration.seconds(5))) // => -5000
Duration.toMillis(Duration.abs(Duration.seconds(-5))) // => 5000

Select the shorter or longer of two durations.

import { Duration } from "effect"
Duration.toSeconds(Duration.min(Duration.seconds(5), Duration.seconds(3))) // => 3
Duration.toSeconds(Duration.max(Duration.seconds(5), Duration.seconds(3))) // => 5

Constrain a duration to an inclusive [minimum, maximum] range.

import { Duration } from "effect"
Duration.toSeconds(
Duration.clamp(Duration.seconds(10), {
minimum: Duration.seconds(2),
maximum: Duration.seconds(5)
})
) // => 5

true if a duration is within an inclusive range.

import { Duration } from "effect"
Duration.between(Duration.seconds(3), {
minimum: Duration.seconds(2),
maximum: Duration.seconds(5)
}) // => true

An Order<Duration> (NegativeInfinity < finite < Infinity). Use it with sorting and Array/Order utilities.

import { Array, Duration } from "effect"
Array.sort([Duration.seconds(3), Duration.seconds(1), Duration.seconds(2)], Duration.Order)
.map(Duration.toSeconds)
// => [1, 2, 3]

Value equality (independent of internal representation).

import { Duration } from "effect"
Duration.Equivalence(Duration.seconds(5), Duration.millis(5000)) // => true
Duration.equals(Duration.seconds(5), Duration.millis(5000)) // => true

isLessThan / isLessThanOrEqualTo / isGreaterThan / isGreaterThanOrEqualTo

Section titled “isLessThan / isLessThanOrEqualTo / isGreaterThan / isGreaterThanOrEqualTo”

Pairwise comparisons (dual).

import { Duration } from "effect"
Duration.isLessThan(Duration.seconds(3), Duration.seconds(5)) // => true
Duration.isLessThanOrEqualTo(Duration.seconds(5), Duration.seconds(5)) // => true
Duration.isGreaterThan(Duration.seconds(5), Duration.seconds(3)) // => true
Duration.isGreaterThanOrEqualTo(Duration.seconds(5), Duration.seconds(5)) // => true

Match on the underlying representation: millis, nanos, or infinity. onNegativeInfinity is optional and falls back to onInfinity.

import { Duration } from "effect"
Duration.match(Duration.seconds(5), {
onMillis: (millis) => `${millis} milliseconds`,
onNanos: (nanos) => `${nanos} nanoseconds`,
onInfinity: () => "infinite"
}) // => "5000 milliseconds"

Match on two durations at once. Effect picks the larger-precision representation: if either side is nanosecond-backed, both are handed to onNanos; otherwise onMillis; any infinity routes to onInfinity.

import { Duration } from "effect"
Duration.matchPair(Duration.seconds(3), Duration.seconds(2), {
onMillis: (a, b) => a + b,
onNanos: (a, b) => Number(a + b),
onInfinity: () => Infinity
}) // => 5000

These come from the Combiner / Reducer traits and are consumed by APIs that fold over many durations (for example via Array/Iterable reduction).

Sums many durations, starting from Duration.zero. combineAll folds an iterable; combineAll([]) returns Duration.zero.

import { Duration } from "effect"
Duration.ReducerSum.combineAll([
Duration.seconds(1),
Duration.seconds(2),
Duration.seconds(3)
]).pipe(Duration.toSeconds) // => 6

Keep the longest or shortest duration when combining.

import { Duration } from "effect"
Duration.CombinerMax.combine(Duration.seconds(2), Duration.seconds(5)).pipe(Duration.toSeconds) // => 5
Duration.CombinerMin.combine(Duration.seconds(2), Duration.seconds(5)).pipe(Duration.toSeconds) // => 2
  • Duration — the public value (Equal, Pipeable, Inspectable).
  • DurationValue — the tagged internal representation: { _tag: "Millis"; millis } | { _tag: "Nanos"; nanos } | { _tag: "Infinity" } | { _tag: "NegativeInfinity" }.
  • Unit — string unit names accepted in inputs ("second"/"seconds", …).
  • Input — everything an API can decode into a Duration (see Duration.Input).
  • DurationObject — additive object input with optional weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds.

Safely build a DateTime from an input, returning Option. Inputs: a DateTime, a Date, epoch number, a parts object, or a parseable string. A zoned input is preserved as Zoned; everything else becomes Utc.

import { DateTime } from "effect"
DateTime.make("2024-01-01")._tag // => "Some"
DateTime.make("not a date")._tag // => "None"

Like make but throws on invalid input.

import { DateTime } from "effect"
DateTime.formatIso(DateTime.makeUnsafe({ year: 2024 }))
// => "2024-01-01T00:00:00.000Z"

Safely build a DateTime.Zoned. By default the input is a UTC instant and the zone is attached without moving it; with adjustForTimeZone: true the input is treated as wall-clock time in the zone.

import { DateTime } from "effect"
DateTime.makeZoned("2024-06-15T14:30:00Z", { timeZone: "Europe/London" })._tag
// => "Some"

Like makeZoned but throws on invalid input.

import { DateTime } from "effect"
DateTime.formatIsoZoned(
DateTime.makeZonedUnsafe("2024-06-15T14:30:00Z", { timeZone: "Europe/London" })
) // => "2024-06-15T15:30:00.000+01:00[Europe/London]"

Parse a zoned ISO string (offset-only or [Time/Zone] form) into Option<Zoned>.

import { DateTime } from "effect"
DateTime.makeZonedFromString("2024-01-01T12:00:00+02:00[Europe/Berlin]")._tag
// => "Some"
DateTime.makeZonedFromString("invalid")._tag // => "None"

Build a Utc from a JavaScript Date (throws on an invalid Date).

import { DateTime } from "effect"
DateTime.formatIso(DateTime.fromDateUnsafe(new Date("2024-01-01T12:00:00Z")))
// => "2024-01-01T12:00:00.000Z"

Read the current instant. now is an Effect<Utc> from the Clock; nowAsDate returns an Effect<Date>; nowUnsafe reads Date.now() directly (prefer the Clock-based versions in Effect code).

import { DateTime, Effect } from "effect"
const eff = DateTime.now // => Effect<DateTime.Utc>
const asDate = DateTime.nowAsDate // => Effect<Date>
const sync = DateTime.nowUnsafe() // => DateTime.Utc

Effect<Zoned, never, CurrentTimeZone> — current instant in the ambient zone.

import { DateTime } from "effect"
DateTime.nowInCurrentZone.pipe(DateTime.withCurrentZoneNamed("Asia/Tokyo"))
// => Effect<DateTime.Zoned>

isDateTime / isTimeZone / isTimeZoneOffset / isTimeZoneNamed

Section titled “isDateTime / isTimeZone / isTimeZoneOffset / isTimeZoneNamed”

Narrow unknown values to DateTime / TimeZone and its variants.

import { DateTime } from "effect"
DateTime.isDateTime(DateTime.makeUnsafe(0)) // => true
const zone = DateTime.zoneMakeOffset(3_600_000)
DateTime.isTimeZone(zone) // => true
DateTime.isTimeZoneOffset(zone) // => true
DateTime.isTimeZoneNamed(zone) // => false

Narrow a known DateTime to its variant.

import { DateTime } from "effect"
DateTime.isUtc(DateTime.makeUnsafe(0)) // => true
DateTime.isZoned(DateTime.makeZonedUnsafe(0, { timeZone: "UTC" })) // => true

Attach a TimeZone (or a fixed offset in ms) to a DateTime, returning Zoned.

import { DateTime } from "effect"
const z = DateTime.zoneMakeNamedUnsafe("Europe/London")
DateTime.setZone(DateTime.makeUnsafe(0), z) // => DateTime.Zoned
DateTime.setZoneOffset(DateTime.makeUnsafe(0), 3 * 60 * 60 * 1000) // => DateTime.Zoned (+03:00)

Attach a named zone by IANA id. The safe version returns Option; the unsafe version throws on an invalid id.

import { DateTime } from "effect"
DateTime.setZoneNamed(DateTime.makeUnsafe(0), "Europe/London")._tag // => "Some"
DateTime.setZoneNamedUnsafe(DateTime.makeUnsafe(0), "Europe/London") // => DateTime.Zoned

Drop the zone, keeping the same instant, returning Utc.

import { DateTime } from "effect"
const zoned = DateTime.makeZonedUnsafe(0, { timeZone: "Europe/London" })
DateTime.formatIso(DateTime.toUtc(zoned)) // => "1970-01-01T00:00:00.000Z"

zoneMakeNamed / zoneMakeNamedUnsafe / zoneMakeNamedEffect

Section titled “zoneMakeNamed / zoneMakeNamedUnsafe / zoneMakeNamedEffect”

Construct a TimeZone.Named from an IANA id — as Option, throwing, or as an Effect failing with IllegalArgumentError.

import { DateTime } from "effect"
DateTime.zoneMakeNamed("Europe/London")._tag // => "Some"
DateTime.zoneToString(DateTime.zoneMakeNamedUnsafe("Asia/Tokyo")) // => "Asia/Tokyo"
DateTime.zoneMakeNamedEffect("Europe/London") // => Effect<TimeZone.Named, IllegalArgumentError>

Construct a fixed-offset zone (offset in ms), or the system’s local named zone.

import { DateTime } from "effect"
DateTime.zoneToString(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) // => "+03:00"
DateTime.zoneMakeLocal() // => TimeZone.Named (system zone)

Parse a zone from an IANA id or ±HH:MM offset (Option), and render one back.

import { DateTime } from "effect"
DateTime.zoneFromString("Europe/London")._tag // => "Some"
DateTime.zoneFromString("+03:00")._tag // => "Some"
DateTime.zoneToString(DateTime.zoneMakeOffset(0)) // => "+00:00"

Read a Zoned value’s offset, in milliseconds or as an ISO ±HH:MM string.

import { DateTime } from "effect"
const z = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", {
timeZone: DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)
})
DateTime.zonedOffset(z) // => 10800000
DateTime.zonedOffsetIso(z) // => "+03:00"

The CurrentTimeZone service supplies a zone to the *Current* helpers, so you do not have to pass it everywhere.

The Context.Service holding the ambient TimeZone.

import { DateTime, Effect } from "effect"
Effect.gen(function* () {
const zone = yield* DateTime.CurrentTimeZone
console.log(DateTime.zoneToString(zone))
}).pipe(DateTime.withCurrentZoneNamed("Europe/London"))

Attach the ambient zone to a DateTime.

import { DateTime, Effect } from "effect"
Effect.gen(function* () {
const now = yield* DateTime.now
const zoned = yield* DateTime.setZoneCurrent(now) // => DateTime.Zoned
}).pipe(DateTime.withCurrentZoneNamed("Europe/London"))
withCurrentZone / withCurrentZoneLocal / withCurrentZoneOffset / withCurrentZoneNamed
Section titled “withCurrentZone / withCurrentZoneLocal / withCurrentZoneOffset / withCurrentZoneNamed”

Provide CurrentTimeZone to an effect from a TimeZone, the system zone, a fixed offset (ms), or an IANA id (the named version may fail with IllegalArgumentError).

import { DateTime, Effect } from "effect"
const z = DateTime.zoneMakeNamedUnsafe("Europe/London")
DateTime.nowInCurrentZone.pipe(DateTime.withCurrentZone(z))
DateTime.nowInCurrentZone.pipe(DateTime.withCurrentZoneLocal)
DateTime.nowInCurrentZone.pipe(DateTime.withCurrentZoneOffset(3 * 60 * 60 * 1000))
DateTime.nowInCurrentZone.pipe(DateTime.withCurrentZoneNamed("Asia/Tokyo"))
layerCurrentZone / layerCurrentZoneOffset / layerCurrentZoneNamed / layerCurrentZoneLocal
Section titled “layerCurrentZone / layerCurrentZoneOffset / layerCurrentZoneNamed / layerCurrentZoneLocal”

Layer versions for app-wide wiring (the named layer can fail with IllegalArgumentError).

import { DateTime } from "effect"
DateTime.layerCurrentZone(DateTime.zoneMakeNamedUnsafe("Europe/London"))
DateTime.layerCurrentZoneOffset(3 * 60 * 60 * 1000)
DateTime.layerCurrentZoneNamed("Europe/London")
DateTime.layerCurrentZoneLocal

Order by epoch milliseconds; equivalence is instant-based regardless of zone.

import { Array, DateTime } from "effect"
Array.sort(
[DateTime.makeUnsafe("2024-03-01"), DateTime.makeUnsafe("2024-01-01")],
DateTime.Order
) // => [2024-01-01, 2024-03-01]
DateTime.Equivalence(
DateTime.makeUnsafe("2024-01-01T12:00:00Z"),
DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", { timeZone: "Europe/London" })
) // => true

Constrain or select instants; between is an inclusive range check.

import { DateTime } from "effect"
const min = DateTime.makeUnsafe("2024-01-01")
const max = DateTime.makeUnsafe("2024-12-31")
DateTime.min(min, max) // => 2024-01-01
DateTime.max(min, max) // => 2024-12-31
DateTime.clamp(DateTime.makeUnsafe("2025-06-15"), { minimum: min, maximum: max }) // => 2024-12-31
DateTime.between(DateTime.makeUnsafe("2024-06-15"), { minimum: min, maximum: max }) // => true

isGreaterThan / isGreaterThanOrEqualTo / isLessThan / isLessThanOrEqualTo

Section titled “isGreaterThan / isGreaterThanOrEqualTo / isLessThan / isLessThanOrEqualTo”

Pairwise comparisons (dual).

import { DateTime } from "effect"
const a = DateTime.makeUnsafe("2024-02-01")
const b = DateTime.makeUnsafe("2024-01-01")
DateTime.isGreaterThan(a, b) // => true
DateTime.isGreaterThanOrEqualTo(b, b) // => true
DateTime.isLessThan(b, a) // => true
DateTime.isLessThanOrEqualTo(b, b) // => true

Compare against “now”. The effectful versions read the Clock; the *Unsafe versions read Date.now() synchronously.

import { DateTime, Effect } from "effect"
const later = DateTime.add(DateTime.nowUnsafe(), { hours: 1 })
DateTime.isFutureUnsafe(later) // => true
DateTime.isPastUnsafe(later) // => false
Effect.gen(function* () {
return yield* DateTime.isFuture(later) // => Effect<boolean>
})

Signed Duration between two instants (positive when other is later).

import { DateTime } from "effect"
DateTime.distance(
DateTime.makeUnsafe(0),
DateTime.makeUnsafe(60_000)
) // => Duration "1m"

Calendar-aware part arithmetic. Day/week/month/year amounts respect the zone.

import { DateTime } from "effect"
DateTime.formatIso(DateTime.add(DateTime.makeUnsafe(0), { days: 1 }))
// => "1970-01-02T00:00:00.000Z"
DateTime.formatIso(DateTime.subtract(DateTime.makeUnsafe(0), { minutes: 5 }))
// => "1969-12-31T23:55:00.000Z"

Elapsed-time math (always milliseconds, not calendar-aware). Accepts a Duration.Input.

import { DateTime, Duration } from "effect"
DateTime.makeUnsafe(0).pipe(DateTime.addDuration("5 minutes")) // => +5m instant
DateTime.makeUnsafe(0).pipe(DateTime.subtractDuration(Duration.hours(1))) // => -1h instant

Adjust calendar fields with a mutable Date copy — in the value’s own zone, or in UTC.

import { DateTime } from "effect"
DateTime.formatIso(
DateTime.mutate(DateTime.makeUnsafe("2024-01-01T12:00:00Z"), (d) => {
d.setHours(15)
d.setMinutes(30)
})
) // => "2024-01-01T15:30:00.000Z"

Transform the underlying epoch milliseconds directly.

import { DateTime } from "effect"
DateTime.makeUnsafe(0).pipe(DateTime.mapEpochMillis((ms) => ms + 10))
// => instant at 10ms

Round to the start, end, or nearest boundary of a unit. For "week", pass { weekStartsOn } (0 = Sunday).

import { DateTime } from "effect"
DateTime.makeUnsafe("2024-01-01T12:00:00Z").pipe(DateTime.startOf("day"), DateTime.formatIso)
// => "2024-01-01T00:00:00.000Z"
DateTime.makeUnsafe("2024-01-01T12:00:00Z").pipe(DateTime.endOf("day"), DateTime.formatIso)
// => "2024-01-01T23:59:59.999Z"
DateTime.makeUnsafe("2024-01-01T12:01:00Z").pipe(DateTime.nearest("day"), DateTime.formatIso)
// => "2024-01-02T00:00:00.000Z"

Strip the time, keeping just the (zone-adjusted) date as Utc.

import { DateTime } from "effect"
DateTime.makeUnsafe("2024-01-01T23:30:00Z").pipe(DateTime.removeTime, DateTime.formatIso)
// => "2024-01-01T00:00:00.000Z"

Run a function against a Date view — zone-adjusted, or UTC — and return its result.

import { DateTime } from "effect"
DateTime.makeUnsafe(0).pipe(DateTime.withDateUtc((d) => d.getTime())) // => 0

Get all calendar parts (with weekDay) — zone-adjusted, or always UTC.

import { DateTime } from "effect"
DateTime.toParts(DateTime.makeUnsafe("2024-01-01T12:30:45.123Z"))
// => { year: 2024, month: 1, day: 1, hour: 12, minute: 30, second: 45, millisecond: 123, weekDay: 1 }

Read one part by key — zone-adjusted, or UTC.

import { DateTime } from "effect"
DateTime.getPartUtc(DateTime.makeUnsafe({ year: 2024 }), "year") // => 2024

Return a new value with the given parts replaced — zone-adjusted, or UTC.

import { DateTime } from "effect"
DateTime.formatIso(
DateTime.setPartsUtc(DateTime.makeUnsafe("2024-01-01T12:00:00Z"), { year: 2025, hour: 18 })
) // => "2025-01-01T18:00:00.000Z"

Convert to a native Date — zone-adjusted wall-clock, or the raw UTC instant.

import { DateTime } from "effect"
DateTime.toDateUtc(DateTime.makeUnsafe("2024-01-01T12:00:00Z")).toISOString()
// => "2024-01-01T12:00:00.000Z"

Get milliseconds since the Unix epoch (always UTC).

import { DateTime } from "effect"
DateTime.toEpochMillis(DateTime.makeUnsafe("2024-01-01T00:00:00Z"))
// => 1704067200000

Handle Utc and Zoned cases separately.

import { DateTime } from "effect"
DateTime.match(DateTime.makeUnsafe("2024-01-01T12:00:00Z"), {
onUtc: (utc) => `UTC: ${DateTime.formatIso(utc)}`,
onZoned: (zoned) => `Zoned: ${DateTime.formatIsoZoned(zoned)}`
}) // => "UTC: 2024-01-01T12:00:00.000Z"

Intl.DateTimeFormat-based formatting. format uses the value’s zone (UTC for Utc); formatLocal uses the system zone/locale; formatUtc forces UTC. All accept Intl.DateTimeFormatOptions plus an optional locale.

import { DateTime } from "effect"
const dt = DateTime.makeZonedUnsafe("2024-06-15T14:30:00Z", { timeZone: "Europe/London" })
DateTime.format(dt, { dateStyle: "full", timeStyle: "short", locale: "en-US" })
// => "Saturday, June 15, 2024 at 3:30 PM"

Format using an existing Intl.DateTimeFormat instance (it controls locale/zone/options).

import { DateTime } from "effect"
const fmt = new Intl.DateTimeFormat("de-DE", { dateStyle: "long", timeZone: "Europe/Berlin" })
DateTime.formatIntl(DateTime.makeUnsafe("2024-06-15T14:30:00Z"), fmt)
// => "15. Juni 2024"

UTC ISO 8601 string.

import { DateTime } from "effect"
DateTime.formatIso(DateTime.makeUnsafe("2024-01-01T12:30:45.123Z"))
// => "2024-01-01T12:30:45.123Z"

Date-only ISO string — zone-adjusted, or always UTC.

import { DateTime } from "effect"
const z = DateTime.makeZonedUnsafe("2024-01-01T23:30:00Z", { timeZone: "Pacific/Auckland" })
DateTime.formatIsoDate(z) // => "2024-01-02" (next day in Auckland)
DateTime.formatIsoDateUtc(z) // => "2024-01-01"

ISO string including the offset (same as formatIso for Utc).

import { DateTime } from "effect"
const z = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", {
timeZone: DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)
})
DateTime.formatIsoOffset(z) // => "2024-01-01T15:00:00.000+03:00"

Full zoned string: YYYY-MM-DDTHH:mm:ss.sss+HH:MM[Time/Zone].

import { DateTime } from "effect"
DateTime.formatIsoZoned(
DateTime.makeZonedUnsafe("2024-06-15T14:30:45.123Z", { timeZone: "Europe/London" })
) // => "2024-06-15T15:30:45.123+01:00[Europe/London]"
  • DateTimeUtc | Zoned.
  • Utc — instant with no zone (_tag: "Utc").
  • Zoned — instant plus a TimeZone (_tag: "Zoned").
  • TimeZoneTimeZone.Offset (fixed offset in ms) or TimeZone.Named (IANA id).
  • Disambiguation"compatible" | "earlier" | "later" | "reject" for DST gaps/overlaps.
  • DateTime.Parts / PartsWithWeekday — calendar components (one-based month); PartsWithWeekday adds weekDay (0 = Sunday).
  • DateTime.PartsForMath — plural amounts (days, weeks, months, years, …) accepted by add / subtract.
  • Clock — read “now” deterministically.
  • TestClock — control time in tests.
  • Schedule — drive retries and repeats with Durations.