phacility/phabricator · error · Exception
ISO8601 date ends in "Z" indicating UTC, but a timezone othe
Error message
ISO8601 date ends in "Z" indicating UTC, but a timezone other than UTC ("%s") was specified. What it means
PhutilCalendarAbsoluteDateTime::newFromISO8601() throws when the parsed value ends in 'Z' (which asserts UTC) but a $timezone argument other than 'UTC' was also supplied. The two are contradictory, so the constructor refuses rather than silently choosing one.
Source
Thrown at src/applications/calendar/parser/data/PhutilCalendarAbsoluteDateTime.php:35
'(?P<y>\d{4})(?P<m>\d{2})(?P<d>\d{2})'.
'(?:'.
'T(?P<h>\d{2})(?P<i>\d{2})(?P<s>\d{2})(?<z>Z)?'.
')?'.
'\z/';
$matches = null;
$ok = preg_match($pattern, $value, $matches);
if (!$ok) {
throw new Exception(
pht(
'Expected ISO8601 datetime in the format "19990105T112233Z", '.
'found "%s".',
$value));
}
if (isset($matches['z'])) {
if ($timezone != 'UTC') {
throw new Exception(
pht(
'ISO8601 date ends in "Z" indicating UTC, but a timezone other '.
'than UTC ("%s") was specified.',
$timezone));
}
}
$datetime = id(new self())
->setYear((int)$matches['y'])
->setMonth((int)$matches['m'])
->setDay((int)$matches['d'])
->setTimezone($timezone);
if (isset($matches['h'])) {
$datetime
->setHour((int)$matches['h'])
->setMinute((int)$matches['i'])
->setSecond((int)$matches['s']);View on GitHub (pinned to 5720a38cfe)
Solutions
- Pass 'UTC' (or omit the parameter, UTC is the default) whenever the value ends in Z.
- If you need local time, keep timezone 'UTC' here and convert afterwards with setTimezone()/toLocalDateTime-style APIs.
- Strip the trailing Z and pre-convert the instant into the target timezone if you must pass a non-UTC zone.
Example fix
// before
$dt = PhutilCalendarAbsoluteDateTime::newFromISO8601('19990105T112233Z', 'America/New_York');
// after
$dt = PhutilCalendarAbsoluteDateTime::newFromISO8601('19990105T112233Z', 'UTC'); Defensive patterns
Strategy: validation
Validate before calling
// Reconcile the timezone with the value before parsing:
if (strtoupper(substr($value, -1)) === 'Z') {
$timezone = 'UTC';
}
$dt = PhutilCalendarAbsoluteDateTime::newFromISO8601($value, $timezone); Type guard
function timezoneMatchesValue($value, $timezone) {
if (strtoupper(substr(trim($value), -1)) === 'Z') {
return strcasecmp($timezone, 'UTC') === 0;
}
return true;
} Try / catch
try {
$dt = PhutilCalendarAbsoluteDateTime::newFromISO8601($value, $timezone);
} catch (Exception $ex) {
// Z + non-UTC conflict: retry in UTC, then convert
$dt = PhutilCalendarAbsoluteDateTime::newFromISO8601($value, 'UTC');
} Prevention
- Default to UTC and only pass another timezone for values without a Z suffix.
- Convert after construction (setTimezone) instead of mixing zone hints at parse time.
When it happens
Trigger: newFromISO8601('19990105T112233Z', 'America/New_York'). Common when a helper always passes a default timezone while values arrive with Z suffixes.
Common situations: Reusable wrappers hardcoding a local timezone; ICS data mixing UTC (Z) values with a per-calendar timezone default; refactors that added a timezone parameter to existing call sites.
Related errors
- Expected ISO8601 datetime in the format "19990105T112233Z",
- Expected ISO8601 duration in the format "P12DT3H4M5S", found
- RRULE dictionary includes unknown key "%s". Expected keys ar
- Unexpected value "%s" in "%s" RULE property: expected an int
- Unexpected value "%s" in "%s" RRULE property: expected only
AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21).
Data as JSON: /api/errors/6d481329c660d0f2.
Report an issue: GitHub.