Skip to the guide
All guides

Error handling

Which calls throw and which never do, the exception hierarchy, the stable ErrorCode values, and how to catch and log failures safely.

app RTLY-Kit 0.2.0checked reading 7 minutes

On this page

Two failure styles

Each function in RTLY-Kit uses one of two styles, so you always know what to guard:

  • Validators never throw. NationalCode , Sheba , BankCard , Mobile , PostalCode and VehiclePlate (and the is_* and validate_* helpers) accept any value and answer with false or an invalid Result . See Validators overview .
  • Everything else throws a RtlyKit\Exceptions\RtlyKitException (or a subclass) for input it cannot accept. A TypeError , ValueError or PHP DateMalformed* exception never escapes for bad input. If you see one, it is a library bug, and we would like a report.

Exception hierarchy

Text
\InvalidArgumentException
 └─ RtlyKit\Exceptions\RtlyKitException        implements RtlyKitThrowable
     ├─ InvalidDateException
     ├─ InvalidNumberException
     ├─ InvalidPrayerConfigException
     └─ UnsupportedLocaleException

RtlyKitThrowable is a marker interface. It declares getErrorCode(): ErrorCode and getContext(): array. Catch it, or RtlyKitException, to handle the whole library with one clause. RtlyKitException extends \InvalidArgumentException, so existing catch (\InvalidArgumentException) and catch (\LogicException) code keeps working.

ExceptionThrown for
InvalidDateExceptionimpossible dates, years outside the supported range, blank or unreadable strings, huge add*() and sub*() amounts, out-of-range timestamps, over-long format patterns, bad holiday dates and settings
InvalidNumberExceptionnon-numeric or fractional input, NaN and INF, oversized input, numbers that are too large, unknown number words, bad Arabic number options
InvalidPrayerConfigExceptionunknown prayer method, unknown city, invalid Asr factor, invalid tune values
UnsupportedLocaleExceptiona locale the function does not support (for example number_to_words(5, 'de'))
RtlyKitException itselfother bad arguments, such as an invalid or over-long Slugify::make() separator or invalid UTF-8 text, or a missing bundled data table

Error codes

Exception messages are for people and may be reworded in any release. The error code is the stable part. It is an ErrorCode backed string enum, and its case names and values are public API (see API stability).

CaseValueMeaning
InvalidArgumentinvalid_argumentfallback for an invalid argument with no more specific code, such as a wrong option for Arabic number words
InvalidDateinvalid_datemalformed, impossible or unreadable date or time (month 13, 30 Esfand in a non-leap year, 'not a date', a blank string)
DateOutOfRangedate_out_of_rangea year, date, timestamp or add*() and sub*() amount outside the supported range (see Limits)
InvalidNumberinvalid_numberthe value cannot be read as a whole number
NumberTooLargenumber_too_largea number larger than the supported size
NonFiniteNumbernon_finite_numberNaN or INF where a finite number is needed
InvalidNumberWordsinvalid_number_wordsunknown or wrongly ordered number words in NumberToWords::fromWords()
InputTooLonginput_too_longstring input over a documented size cap
InvalidPrayerConfiginvalid_prayer_configunknown city, calculation method or Asr factor, or an invalid tune
UnsupportedLocaleunsupported_localea locale that is not supported
DataUnavailabledata_unavailablea bundled data table is missing or damaged (a packaging problem, not a user error)

Note. The strings returned by Result::errors() (invalid_format, invalid_checksum, invalid_type and so on) are plain strings from the validators, not ErrorCode cases. The enum is for exceptions only.

Reading the code and the context

Every library exception has getErrorCode() and getContext(). The context is an array of facts about the failure that a program can read (a limit, the locale, the name of the argument). It never contains secrets and may be empty. This script feeds five bad inputs to number_to_words():

PHP
<?php
require __DIR__.'/vendor/autoload.php';

use function RtlyKit\number_to_words;
use RtlyKit\Exceptions\RtlyKitThrowable;

foreach ([1.5, NAN, 'abc', str_repeat('9', 22), str_repeat('1', 5000)] as $input) {
    try {
        number_to_words($input);
    } catch (RtlyKitThrowable $e) {
        echo (new ReflectionClass($e))->getShortName(), ' ',
            $e->getErrorCode()->value, ' ',
            json_encode($e->getContext()), "\n";
    }
}
// InvalidNumberException invalid_number []
// InvalidNumberException non_finite_number []
// InvalidNumberException invalid_number []
// InvalidNumberException number_too_large {"limit":"10^21 - 1"}
// InvalidNumberException input_too_long {"limit":4096}

Catching patterns

Branch on the code, not on the message. Use match with a default arm, because a minor release may add new cases.

PHP
<?php
require __DIR__.'/vendor/autoload.php';

use RtlyKit\Calendar\Jalali;
use RtlyKit\Exceptions\{ErrorCode, InvalidDateException, RtlyKitThrowable};

function category(RtlyKitThrowable $e): string
{
    return match ($e->getErrorCode()) {
        ErrorCode::InvalidDate, ErrorCode::DateOutOfRange => 'date',
        ErrorCode::InputTooLong => 'too-long',
        default => 'other',
    };
}

try {
    Jalali::create(1404, 13, 1);          // there is no month 13
} catch (RtlyKitThrowable $e) {
    echo category($e), "\n";               // date
}

$e = InvalidDateException::because(ErrorCode::DateOutOfRange, 'Year is out of range', ['year' => 99999]);
echo $e->getErrorCode()->value, ' ', json_encode($e->getContext()), "\n";
// date_out_of_range {"year":99999}

try {
    Jalali::create(1404, 13, 1);
} catch (\InvalidArgumentException $e) {   // old-style catch still works
    echo get_class($e), ' ', $e instanceof RtlyKitThrowable ? 'is RtlyKitThrowable' : '', "\n";
    // RtlyKit\Exceptions\InvalidDateException is RtlyKitThrowable
}

RtlyKitException::because(ErrorCode, string $message, array $context = [], ?Throwable $previous = null) is the named constructor the library uses to raise an exception with a specific code. You can use it for your own failures too. The code you add this way is your own and not part of the contract of the library.

When you log, write the code and the context and not the message text:

PHP
catch (RtlyKitThrowable $e) {
    error_log($e->getErrorCode()->value.' '.json_encode($e->getContext()));
}

What never throws

The validators accept any PHP value. They raise no exception for bad input, whatever its type or size:

PHP
<?php
require __DIR__.'/vendor/autoload.php';

use function RtlyKit\validate_national_code;
use RtlyKit\Validation\Sheba;

var_dump(Sheba::isValid(null));                                  // bool(false)
echo json_encode(validate_national_code([])->errors()), "\n";                    // ["invalid_type"]
echo json_encode(validate_national_code(str_repeat('1', 5000))->errors()), "\n"; // ["input_too_long"]
echo json_encode(validate_national_code('0013542418')->errors()), "\n";          // ["invalid_checksum"]
var_dump(validate_national_code('0013542419')->isValid());       // bool(true)
Validator inputResult
string up to 4096 byteschecked as usual (Persian and Arabic digits are accepted)
int, whole finite floatturned into a string, then checked (an int has already lost its leading zeros)
null, bool, arrays, objects, NaN, INF, fractional floatsinvalid, error invalid_type
string over 4096 bytesinvalid, error input_too_long
invalid UTF-8invalid (usually invalid_format)

The Laravel validation rules and the Globals::register() opt-in follow the same rule. The rules return a boolean, and Globals::register() never throws. It returns the names it skipped.

Input caps

Every call that takes untrusted text has a size cap, so a hostile input cannot cost much. Going over a cap raises input_too_long, as an exception or as a validator error. The full list is on Limits.

WhereCapWhen exceeded
Validators4096 bytes per stringinvalid Result, error input_too_long
NumberToWords::convert() and fromWords(), Format string input4096 bytes per stringInvalidNumberException
Format::withSeparator() and format_number()1000 characters in the plain decimal numberInvalidNumberException
Slugify::make() separator64 bytes, valid UTF-8RtlyKitException
format() pattern (Jalali, Hijri, Hebrew)256 bytes (MAX_FORMAT_LENGTH on each class)InvalidDateException

The calendar range rule

Every calendar entry point (make, create, createFromFormat, timestamps, add* and sub*, the helpers and the Carbon macros) either returns a valid date or throws InvalidDateException. Nothing wraps around, and no other exception type is used. The ranges are Jalali -620 to 9377, Hijri 1 to 9665 and Hebrew 3762 to 13759, all within Gregorian years 1 to 9999. See Limits.

A complete example

PHP
use function RtlyKit\jdate;
use function RtlyKit\number_to_words;
use RtlyKit\Exceptions\InvalidDateException;
use RtlyKit\Exceptions\RtlyKitThrowable;

try {
    $date  = jdate($userInput);
    $words = number_to_words($userAmount);
} catch (InvalidDateException $e) {
    echo "Please enter a valid date.\n";
} catch (RtlyKitThrowable $e) {
    error_log($e->getErrorCode()->value.' '.json_encode($e->getContext()));
}