Instant vs civil time
The single most important distinction — a point on the timeline vs a reading on a calendar/clock.
There are two fundamentally different things people call “a time”, and mixing them up is the root of most date bugs. (What “a time” is sets up the broader vocabulary; this page draws the line that matters most.)
An instant is an absolute point on the universal timeline — the same moment everywhere on Earth. “2026-06-05T14:00:00Z” names an instant. So does a Unix timestamp. Two observers in different time zones agree on when it happened; they only disagree on what their wall clocks read.
A civil time (also “local time” or “wall-clock time”) is a reading on a human calendar and clock: “June 5th, 3:00 PM”. By itself it does not name a point on the timeline — “3 PM” happened at different absolute instants in Tokyo and in New York. A civil time only becomes an instant when you attach a time zone (or offset).
Different languages have their own name for this zone-less value: Java’s
LocalDate / LocalDateTime, JavaScript’s Temporal.PlainDate, Python’s
“naive” datetime. They all mean the same thing — a calendar/clock reading
with no zone attached. This guide says civil time rather than “local” to
keep it distinct from a time that has already been resolved into a specific
zone, which is what “local time” usually implies.
A civil value becomes an instant only when you attach a zone:
// A civil value becomes an instant only when you attach a zone:
const civil = Temporal.PlainDateTime.from('2026-06-05T15:00'); // no zone yet
const zoned = civil.toZonedDateTime('America/New_York'); // now a point in time
zoned.toInstant().toString(); // '2026-06-05T19:00:00Z'
// Legacy Date can't hold a zone-less civil time — it's always an instant.JS examples use Temporal — standardized in ES2026, available via a polyfill today.
from datetime import datetime
from zoneinfo import ZoneInfo
civil = datetime(2026, 6, 5, 15, 0) # naive: a wall-clock reading, no zone
aware = civil.replace(tzinfo=ZoneInfo('America/New_York')) # attach a zone -> an instant
aware.isoformat() # '2026-06-05T15:00:00-04:00'import java.time.*;
// A civil value becomes an instant only when you attach a zone:
LocalDateTime civil = LocalDateTime.parse("2026-06-05T15:00"); // no zone yet
Instant instant = civil.atZone(ZoneId.of("America/New_York")).toInstant();
instant.toString(); // '2026-06-05T19:00:00Z'
// Legacy java.util.Date can't hold a zone-less civil time — it's always an instant.Java examples use java.time (JSR-310) — see the Java page for the legacy java.util.Date minefield.
import kotlinx.datetime.*
// A civil value becomes an instant only when you attach a zone:
val civil = LocalDateTime.parse("2026-06-05T15:00") // no zone yet
val instant = civil.toInstant(TimeZone.of("America/New_York")) // now a point in time
instant.toString() // '2026-06-05T19:00:00Z'
// Legacy java.util.Date can't hold a zone-less civil time — it's always an instant.Kotlin examples use kotlinx-datetime, JetBrains’ library — separate from the stdlib.
import Foundation
// DateComponents is the civil reading; a Calendar with a zone makes it an instant:
let civil = DateComponents(year: 2026, month: 6, day: 5, hour: 15, minute: 0)
var cal = Calendar(identifier: .gregorian)
cal.timeZone = TimeZone(identifier: "America/New_York")!
let instant = cal.date(from: civil)!
ISO8601DateFormatter().string(from: instant) // '2026-06-05T19:00:00Z'Swift examples use Foundation’s Calendar/DateComponents — see the Swift page.
import (
"fmt"
"time"
)
// A zone-less string is a civil reading; the location you parse it IN makes
// it an instant. Plain time.Parse would silently assume UTC:
ny, _ := time.LoadLocation("America/New_York")
instant, _ := time.ParseInLocation("2006-01-02 15:04", "2026-06-05 15:00", ny)
fmt.Println(instant.UTC()) // 2026-06-05 19:00:00 +0000 UTCGo examples use the standard library’s time package — see the Go page.
use jiff::civil::DateTime;
// A civil value becomes an instant only when you attach a zone:
let civil: DateTime = "2026-06-05T15:00".parse()?; // no zone yet
let zoned = civil.in_tz("America/New_York")?; // now a point in time
println!("{}", zoned.timestamp()); // 2026-06-05T19:00:00ZRust examples use jiff, a third-party crate — Rust’s std has no calendar types at all.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// A struct tm is a civil reading; mktime interprets it in the PROCESS-GLOBAL
// zone (set via TZ + tzset) and returns a time_t instant:
setenv("TZ", "America/New_York", 1);
tzset();
struct tm civil = {0};
civil.tm_year = 2026 - 1900; civil.tm_mon = 6 - 1; civil.tm_mday = 5;
civil.tm_hour = 15; civil.tm_isdst = -1; // -1: let mktime resolve DST
time_t instant = mktime(&civil);
char out[32];
strftime(out, sizeof out, "%Y-%m-%dT%H:%M:%SZ", gmtime(&instant));
printf("%s\n", out); // 2026-06-05T19:00:00ZC examples use <time.h> — see the C page. Standard C has no zone-aware or date-only type; the zone is a process-global set by TZ.
#include <chrono>
#include <format>
#include <iostream>
using namespace std::chrono;
// local_time is civil (no zone); zoned_time attaches a named zone, turning it
// into an absolute sys_time:
auto civil = local_days{June/5/2026} + 15h;
zoned_time zt{"America/New_York", civil};
std::cout << std::format("{:%Y-%m-%dT%H:%M:%S%z}", zt) << "\n"; // civil + offset
std::cout << std::format("{:%Y-%m-%dT%H:%M:%SZ}", zt.get_sys_time()) << "\n"; // UTC instantC++ examples use std::chrono (C++20) — see the C++ page. The pre-C++20 world lived on C’s <time.h>.
Why it matters
Storing a civil time and treating it like an instant (or vice versa) silently corrupts data the moment two time zones are involved.
Pitfall: Storing “2026-06-05 15:00” with no zone, then later assuming it’s UTC. If it was actually local, every downstream calculation is off by the offset — and the error changes across DST boundaries.
A rule of thumb
- Recording when something happened → store an instant (UTC).
- Recording a time on a human calendar (a 9 AM meeting, a store’s opening hours) → store a civil time + the zone it’s interpreted in, not a UTC instant. The instant changes when DST rules change; the civil intent does not.
Go deeper: why "future meeting in UTC" is a bug
If you convert a future civil time to a UTC instant now and store only that instant, a later change to that zone’s DST rules (governments change them) will make your stored instant point to the wrong wall-clock time. Store the civil time + zone id and resolve to an instant at the last responsible moment.
See also Time zones vs offsets, Store vs display, and Naming time fields.