kotlin.time.Duration
value class ยท unit-safe ยท .seconds / .millisecondsDuration represents a span of time โ not a point in time โ and it exists specifically to stop bugs caused by an unlabeled Long that might mean milliseconds, seconds, or nanoseconds depending on which API you're calling. It's a value class under the hood, so wrapping a number this way costs essentially nothing at runtime while making unit mismatches a compile-time impossibility.
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.minutes // Extension properties on Int/Long build a Duration directly โ // the unit is part of the value, not a separate parameter to misread val timeout = 30.seconds val pollInterval = 500.milliseconds val sessionLength = 15.minutes // Arithmetic and comparison work directly, unit conversion is automatic val total = timeout + pollInterval println(total) // 30.5s println(total < sessionLength) // true // Convert to a specific unit only when an external API demands a raw number println(timeout.inWholeMilliseconds) // 30000
Duration maps directly onto Go's time.Duration โ both are a typed wrapper around a raw number specifically to prevent "was that milliseconds or seconds?" bugs, and both offer unit-labeled constructors (30 * time.Second vs Kotlin's 30.seconds).measureTime & measureTimedValue
benchmarking a blockTiming a block of code by hand means capturing System.nanoTime() before and after and subtracting โ easy to get slightly wrong, and it returns a raw Long with no unit attached. measureTime wraps that pattern and returns a proper Duration directly; measureTimedValue does the same but also keeps the block's actual return value alongside the timing.
import kotlin.time.measureTime import kotlin.time.measureTimedValue // measureTime: just the duration, block's result discarded val elapsed = measureTime { (1..1_000_000).sum() } println("took $elapsed") // e.g. "took 8.2ms" // measureTimedValue: both the duration AND the block's result val (result, duration) = measureTimedValue { (1..1_000_000).sum() } println("result=$result, took=$duration")
kotlinx-datetime: Instant & LocalDateTime
multiplatform ยท point in time vs civil timekotlinx-datetime is the official multiplatform date/time library โ needed because java.time only exists on the JVM, and Kotlin/JS and Kotlin/Native have nothing equivalent built in. Its central distinction mirrors java.time's own: Instant is an unambiguous point on the global timeline (nanoseconds since the epoch), while LocalDateTime is "civil time" โ a date and time with no attached time zone, the kind of value a calendar app shows you.
import kotlinx.datetime.Clock import kotlinx.datetime.Instant // Clock.System.now() returns the current Instant โ same moment // everywhere on Earth, regardless of the caller's time zone val now: Instant = Clock.System.now() val later = now + kotlin.time.Duration.parse("1h") println(later > now) // true: Instants compare directly, no ambiguity val parsed = Instant.parse("2024-03-15T10:30:00Z") // ISO-8601, "Z" = UTC
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.TimeZone // "March 15th, 2024, 10:30 AM" โ meaningless without a time zone // to say WHERE that clock reading applies val civilTime = LocalDateTime(2024, 3, 15, 10, 30) println(civilTime) // 2024-03-15T10:30 // An Instant only becomes civil time once you attach a zone val now = kotlinx.datetime.Clock.System.now() val localNow = now.toLocalDateTime(TimeZone.currentSystemDefault()) println(localNow)
Instant; only convert to LocalDateTime at the point where you're displaying a value to a human in a specific place. Mixing the two โ comparing a LocalDateTime from one zone against one from another as if they were on the same timeline โ is a recurring source of off-by-hours bugs in date/time code across every language, not just Kotlin.TimeZone
IANA identifiers ยท currentSystemDefaultTimeZone is what converts between an unambiguous Instant and a zone-specific LocalDateTime, in both directions. It's identified by IANA database names like "America/New_York" โ never by a fixed UTC offset alone, since a named zone accounts for daylight saving transitions that a plain offset can't.
import kotlinx.datetime.* val now = Clock.System.now() val tokyo = TimeZone.of("Asia/Tokyo") val newYork = TimeZone.of("America/New_York") // Same Instant, different LocalDateTime depending on the zone println(now.toLocalDateTime(tokyo)) println(now.toLocalDateTime(newYork)) // The reverse: interpret civil time AS a specific zone, get back an Instant val meetingTime = LocalDateTime(2024, 3, 15, 9, 0) val meetingInstant = meetingTime.toInstant(newYork)
java.time Interop
toJavaInstant ยท toKotlinLocalDateTimeOn the JVM specifically, plenty of existing libraries (JDBC drivers, older APIs) still speak java.time types directly. kotlinx-datetime provides explicit conversion functions rather than making its own types secretly interchangeable with Java's, which keeps the conversion visible in the code wherever it happens.
import kotlinx.datetime.Clock import kotlinx.datetime.toJavaInstant import kotlinx.datetime.toKotlinInstant import java.time.Instant as JavaInstant val kotlinNow = Clock.System.now() // kotlinx.datetime.Instant -> java.time.Instant, for a Java-facing API val javaInstant: JavaInstant = kotlinNow.toJavaInstant() // java.time.Instant -> kotlinx.datetime.Instant, coming back the other way val backToKotlin = javaInstant.toKotlinInstant()
java.time directly (via ordinary Java interop, no special Kotlin wrapping needed) is a perfectly reasonable choice and avoids an extra dependency. Reach for kotlinx-datetime specifically when multiplatform support matters, or when its API ergonomics (like Duration integration) are worth the added dependency on a JVM-only project too.Quick Reference
Syntax cheat-sheet| Concept | Syntax | Notes |
|---|---|---|
| Build a Duration | 30.seconds / 500.milliseconds | Unit is part of the value, not a separate parameter |
| Convert to raw units | duration.inWholeMilliseconds | Only needed at an API boundary demanding a raw number |
| Time a block | measureTime { ... } | Returns a Duration directly |
| Time + keep the result | measureTimedValue { ... } | Returns (value, duration) |
| Unambiguous moment | Clock.System.now(): Instant | Same point on the timeline everywhere |
| Civil time, no zone | LocalDateTime(y, m, d, h, min) | Meaningless without a TimeZone attached |
| Instant โ civil time | instant.toLocalDateTime(zone) | Requires an explicit TimeZone |
| Civil time โ Instant | localDateTime.toInstant(zone) | Interprets the civil time as being in that zone |
| Named time zone | TimeZone.of("America/New_York") | IANA name, not a fixed UTC offset |
| JVM interop | kotlinInstant.toJavaInstant() | Explicit conversion, not implicit interchangeability |