skip to content

In Dart, how do UTC and local DateTime values differ, and how should a booking app parse, store and compare check-in times?

level: middleimportance: must knowfreq 55%

answer

  1. only two zones: UTC or local
  2. isUtc, toUtc(), toLocal()
  3. parse: offset or Z gives UTC
  4. == also compares the zone
  5. isAtSameMomentAs for instants

basics

~20 s

A DateTime is anchored either in UTC or in the device's local zone, shown by isUtc. Store and send instants in UTC, convert with toLocal() only for display, and compare with isAtSameMomentAs or compareTo, since == also requires the same zone.

solid answer

~40 s

Dart's `DateTime` knows only two zones: UTC and the local zone of the machine running the code. `DateTime.now()` and `DateTime(y, m, d)` are local; `DateTime.utc(...)` and `DateTime.timestamp()` are UTC; `toUtc()` and `toLocal()` give the same instant in the other zone, and `isUtc` tells you which you have. `DateTime.parse` returns UTC when the string has `Z` or an offset, converting any offset to UTC, and local time when it has neither. For a booking app I send and store `toUtc().toIso8601String()`, which ends in `Z`, and convert to local only in the UI. Comparisons need care: `==` is true only for the same moment in the same zone, so a UTC value and its `toLocal()` twin are not equal; use `isAtSameMomentAs`, `isBefore`, `isAfter` or `compareTo`.

code

dart · 14 lines
dart
void main() {
  final fromApi = DateTime.parse('2026-10-24T14:00:00+02:00');
  print(fromApi.isUtc); // true
  print(fromApi); // 2026-10-24 12:00:00.000Z

  final typed = DateTime.parse('2026-10-24 14:00');
  print(typed.isUtc); // false: no offset means device-local time

  final shown = fromApi.toLocal();
  print(shown == fromApi); // false: same moment, different zone
  print(shown.isAtSameMomentAs(fromApi)); // true
  print(shown.compareTo(fromApi)); // 0
  print(fromApi.toIso8601String()); // 2026-10-24T12:00:00.000Z
}

go deeper

for a junior

Know the two zones, isUtc, and toUtc()/toLocal(), and that DateTime.now() is local while DateTime.timestamp() is UTC.

for a middle

Explain the parse rule for Z, offsets and bare strings, and why == differs from isAtSameMomentAs and compareTo.

for a senior

Set the app's contract: UTC on the wire and in storage, local only in the UI, date-only values modelled separately, and property zones resolved with a real tz database.

for a principal

Decide where time-zone knowledge lives, client or server, and make the API carry offsets so no client ever guesses a zone.

## Two zones, not many A Dart `DateTime` is an **instant** plus a flag saying how to present it. The documentation puts it plainly: a `DateTime` is anchored either in the **UTC** time zone or in the **local time zone** of the current computer when the object is created. There is no way to say "this is 14:00 in Lisbon" with `dart:core` alone. | Constructor or method | Zone of the result | |---|---| | `DateTime.now()`, `DateTime(2026, 10, 24, 14)` | local | | `DateTime.utc(2026, 10, 24, 12)`, `DateTime.timestamp()` | UTC | | `dt.toUtc()` / `dt.toLocal()` | the same instant, other zone | | `DateTime.parse('...Z')` or with `+02:00` | UTC | | `DateTime.parse('2026-10-24 14:00')` | local | `isUtc` reports the flag, `timeZoneName` gives an abbreviation such as `UTC` or a local name, and `timeZoneOffset` gives the local zone's offset as a `Duration`. A `DateTime` is immutable: every conversion returns a new object. ## Parsing rules that bite `DateTime.parse` accepts a subset of ISO 8601 that includes RFC 3339, plus the output of `toString()` and `toIso8601String()`. 1. With a **`Z`**, the result is UTC. 2. With a **numeric offset** such as `+02:00`, the time is converted to the equivalent UTC time, and the offset itself is not kept. 3. With **neither**, the result is in the device's local zone, so the same string means different instants on phones in different zones. 4. Out-of-range components **overflow** rather than fail: `'2020-01-42'` parses as 11 February 2020. To reject such input you need a strict parser from a formatting package. 5. Malformed text throws a `FormatException`; `DateTime.tryParse` returns `null` instead. ## Equality versus ordering - **`==`** is true only when both values are the **same moment and the same zone**. `utc == utc.toLocal()` is `false`. - **`isAtSameMomentAs`**, **`isBefore`**, **`isAfter`** compare instants and ignore the zone flag. - **`compareTo`**, from `Comparable<DateTime>`, returns 0 for the same moment in either zone. The `Comparable` documentation calls out `DateTime` as a type whose `compareTo` does not agree with `==`. So sorting bookings with `list.sort()` works across zones, while a `Set<DateTime>` or map keys built from mixed zones can hold the same instant twice. Normalise to UTC before using dates as keys. ## Serialising and reading back - `toIso8601String()` writes `yyyy-MM-ddTHH:mm:ss.mmm` plus microseconds when non-zero, and a trailing `Z` **only for UTC values**. A local value serialises without any offset, so whoever reads it back gets their own local time. - `toString()` uses a space instead of `T` and is meant for humans and logs, though `parse` accepts it too. - `millisecondsSinceEpoch` and `DateTime.fromMillisecondsSinceEpoch(ms, isUtc: true)` round-trip an instant as a number, which some APIs prefer; pass `isUtc: true` or the result is presented in local time. - A round trip through `toIso8601String()` and `parse` preserves the instant and the UTC flag for UTC values, which is another reason to serialise UTC. ## A booking app's rules - **Send and store UTC.** Serialise with `toUtc().toIso8601String()`, which ends in `Z`, so every reader gets the same instant. - **Parse server values that carry `Z` or an offset**, and reject ones that do not, unless the API documents a zone. - **Display in local time** with `toLocal()`, and remember the user's device zone may not be the hotel's zone. - **Hotel-local rules** such as "check-in from 15:00 at the property" need the property's time zone, which `dart:core` cannot express; that takes a time-zone database package, or a server that sends instants already resolved. - **Date-only values** such as a check-in day are calendar dates, not instants. Represent them as `DateTime.utc(y, m, d)` or as separate fields, never as local midnight shifted through `toUtc()`. ## What interviewers listen for The two-zone model, the parse rule for strings without an offset, and the `==` trap are the core. Strong answers add that `DateTime.timestamp()` gives the current time directly in UTC, and that formatting for a locale is a separate concern handled outside `dart:core`.

  • The API returns '2026-10-24T14:00:00' with no offset. What goes wrong?
    `DateTime.parse` treats it as local time on each device, so a phone in Lisbon and one in Tokyo turn it into different instants. Fix the contract: have the server send `Z` or an explicit offset, or document the zone and convert on the server. Do not guess on the client.
  • Why can a Set<DateTime> contain the same check-in twice?
    `Set` uses `==` and `hashCode`, and `DateTime.==` requires the same zone as well as the same moment. A UTC value and its `toLocal()` twin are different keys. Normalise with `toUtc()` before inserting.
  • How would you show a check-in time in the hotel's zone rather than the phone's?
    `dart:core` cannot, because `DateTime` only knows UTC and the device's local zone. Either use a time-zone database package to convert the UTC instant to the property's zone, or have the server send a display string or offset for the property.

A DateTime is a moment written on a card with one of two stamps: UTC or local. toLocal() rewrites the card with the other stamp. isAtSameMomentAs asks whether the cards name the same moment; == also insists the stamps match.

saying these in an interview costs you the question

  • DateTime.parse keeps the +02:00 offset as the value's zone
  • A string without an offset is parsed as UTC
  • == returns true for a UTC value and its toLocal() copy
  • DateTime can represent any named time zone such as Europe/Lisbon
  • toUtc() changes the instant the value represents