PHP 8.3: Granular DateTime Exceptions

Version8.3
TypeChange

In PHP 8.3, the Date/Time extension introduces extension-specific and granular Exception and Error classes to better convey the error and exception states. This makes it easier and cleaner to catch date-specific exceptions.

Prior to PHP 8.3, the Date/Time extension used standard \Exception and \Error.

The new Exception/Error classes extend existing \Error and \Exception classes, which means existing code that catch \Exception or \Error exceptions should continue to catch these errors.

Note that the Date/Time extension continues to throw \ValueError (when passing invalid values to function/methods), \TypeError (standard type errors), and \Error exceptions (attempting to modify read-only properties, errors in unserializing data, etc).

New Exception Classes

PHP 8.3 adds nine Exception/Error classes to the Date/Time extension. User-land PHP code is allowed to throw these exceptions as well, although these exceptions are reserved for the Date/Time extension by convention.

Hierarchy of the Date/Time Exception classes

The following chart shows the new DateError

Throwable
  ├── Error
  |     └── DateError
  |             ├── DateObjectError
  │             └── DateRangeError
  └── Exception
        └── DateException
                ├── DateInvalidTimeZoneException
                ├── DateInvalidOperationException
                ├── DateMalformedStringException
                ├── DateMalformedIntervalStringException
                └── DateMalformedPeriodStringException

DateError and its sub classes (DateObjectError and DateRangeError) are reserved for the errors with the Date extension itself or the PHP run-time, and are not related to the actual values passed to the Date/Time classes or functions.

DateException and its sub classes are thrown when the values themselves are invalid or malformed. For PHP applications that accept user-provided or dynamic date or time values, the DateException is the appropriate Exception to catch.


DateError

DateError errors are thrown if the underlying timelib database is corrupt. This is an uncommon case, and indicates something wrong with the PHP setup itself.


DateObjectError

DateObjectError errors are thrown when a Date/Time class object is not properly initialized. One common case would be when a user-land PHP class extends a Date/Time class, but does not properly initialize it at the constructor by calling parent::__construct().

class Foo extends DateInterval {
    public function __construct() {
        // Does not call parent::__construct();
    }
}

$interval = new Foo();
$interval->format('s');
DateObjectError: Object of type Foo (inheriting DateInterval) has not been correctly initialized by calling parent::__construct() in its constructor

Further, attempting to compare uninitialized Date/Time objects result in a DateObjectError error as well:

DateObjectError: Trying to compare uninitialized DateTimeZone objects

DateRangeError

DateRangeError errors are thrown when attempting to process a date that exceeds the PHP integer value.

DateRangeError: Epoch doesn't fit in a PHP integer

DateException

DateException exceptions the common type of \Exception sub classes that will be thrown for invalid user-provided values.


DateInvalidTimeZoneException

DateInvalidTimeZoneException exceptions are thrown when attempting to instantiate a DateTimeZone class object with an unrecognized timezone name, or when attempting to set an out of range time zone offset.

new DateTimeZone("DoesNotExists");
new DateTimeZone("-9999");
DateInvalidTimeZoneException: DateTimeZone::__construct(): Unknown or bad timezone (DoesNotExists)
DateInvalidTimeZoneException: DateTimeZone::__construct(): Timezone offset is out of range (-9999)

DateInvalidOperationException

DateInvalidOperationException exceptions are thrown when attempting an invalid operation on a DateTime object. Currently, calling DateTimeInterface::sub on special time specification throws this exception. Prior to PHP 8.3, this condition resulted in a PHP warning.

$now = new DateTimeImmutable("1992-09-16 10:44:00 CET");
$e = DateInterval::createFromDateString('next wednesday');
$now->sub($e);
DateInvalidOperationException: DateTimeImmutable::sub(): Only non-special relative time specifications are supported for subtraction

DateMalformedStringException

DateMalformedStringException exceptions are thrown when the DateTime extension could not parse a valid date/time from the given string. The most common case is when a DateTime/DateTimeImmutable object is instantiated with an invalid date/time string.

new DateTimeImmutable('half-life 3 release date');
DateMalformedStringException: Failed to parse time string (half-life 3 release date) at position 0 (h): The timezone could not be found in the database

DateMalformedIntervalStringException

Similar to DateMalformedStringException, DateMalformedIntervalStringException exceptions are thrown when Date/Time extension classes encounter an invalid interval string.

new DateInterval('until tomorrow');
new DateInterval('1992-09-16T10:44:00Z');
DateInterval::createFromDateString('next wednesday 10:44');
DateMalformedIntervalStringException: Unknown or bad format (until tomorrow)
DateMalformedIntervalStringException: Failed to parse interval (1992-09-16T10:44:00Z)
Uncaught DateMalformedIntervalStringException: String 'next wednesday 10:44' contains non-relative elements

DateMalformedPeriodStringException

DateMalformedPeriodStringException exceptions are thrown when attempting to instantiate DatePeriod class instances with malformed period strings.

new DatePeriod('1 mississippi');
new DatePeriod('10D');
new DatePeriod("R4");
new DatePeriod("2012-07-01T00:00:00Z/P7D");
DateMalformedPeriodStringException: Unknown or bad format (1 mississippi)
DateMalformedPeriodStringException: Unknown or bad format (10D)
DateMalformedPeriodStringException: DatePeriod::__construct(): ISO interval must contain a start date, "R4" given
DateMalformedPeriodStringException: DatePeriod::__construct(): Recurrence count must be greater than 0

Exception Class Polyfill

Although it is not possible to port the new granular exception changes to older PHP versions, it is possible to polyfill the exception/error classes to older PHP versions. This might be helpful in case the application needs to unserialize exception objects previously thrown under a PHP > 8.3 environment. The polyfill can also be helpful in case the PHP application or libraries it uses decides to make use of the new Exception/Error classes.

class DateError extends Error {}

class DateObjectError extends DateError {}
class DateRangeError extends DateError {}

class DateException extends Exception {}

class DateInvalidTimeZoneException extends DateException {}
class DateInvalidOperationException extends DateException {}
class DateMalformedStringException extends DateException {}
class DateMalformedIntervalStringException extends DateException {}
class DateMalformedPeriodStringException extends DateException {}

Backward Compatibility Impact

This is technically a BC break, because certain warnings in PHP < 8.2 are turned into Exceptions in PHP 8.3 and later. The following exceptions replace a warning condition in PHP 8.2 and early versions.


RFC Discussion Implementation