Advanced Custom Exception Classes

Advanced exception classes for fast-fail programming

What Are Custom Exception Classes

PHP offers the convenience of creating custom exceptions that extend the Exception class. These are known as a User-Defined Exception or Custom Exception, and can be customized to the context of the situation that arises in the code.

Why Use Custom Exception Classes

Custom exceptions are used to determine how the system should respond to an exception upline. a custom exception class should have a context specific message and exception code. When a generic exception is used.

eg:

throw new Exception('some unique message here');

the system only knows that an exception occurred but now there is only 1 response possible - catch it and send a message to the calling method or end user.

By defining a custom exception we can have context appropriate responses.

eg:

try {
    //some code here
} catch (InvalidAuthorizationException $exception) {
    event (new InvalidAuthorizationEvent($exception));
} catch (InvalidCSVFormatException | ColumnTypeMismatchException $exception) {
    event (new InvalidFileFormatException($exception));
} catch (InvalidDataException $exception) {
    event (new InvalidDataException($exception));
}

The above example demonstrates how each exception can be handled differently, using a different event handler that is specific to the exception thrown.

The first catch statement might be designed to notify a site administrator that the security of the endpoint is comprimised

The 2nd catch statement might be designed to notify the client that the file is corrupt

The 3rd catch statement might be designed to notify the client that the values provided do not pass validation

Custom Exception Classes - More Than Just Extending

Most basic examples of creating a custom exception for beginners are the bare requirement of how to create the class:

class MyCustomException extends \Exception
{
    public function __construct($message = "", $code = 0, Throwable $previous = null)
    {
        parent::__construct($message, $code, $previous);
    }
}

The usage for the above example is as follows:

throw new MyCustomException('Invalid authorization attempt');

or:

throw new MyCustomException('Invalid authorization attempt', 400);

While these are better than no exception thrown at all, this is no different than:

throw new \Exception('Invalid authorization attempt', 400);

Why? Because the exception class can be passed ANY string into it, and ANY exception code into it - making it a generic exception class with a unique name

Immutable Exception Classes

Custom exception classes should be immutable - this means that cannot be changed to fit the context of each different issue that arises. The custom exception should be defined for 1 type of exception only. It should have a properly defined exception code (if possible) that can be used for creating the response object AND for the client endpoint to check the status of the response code BEFORE determining whether there is an object to display or list of results to iterate.

Consider the following example:

class Constants
{
    public const INVALID_HANDLER_TYPE_EXCEPTION_MESSAGE = 'Invalid handler specified for filetype :filetype';

    public const INVALID_HANDLER_TYPE_EXCEPTION_CODE = 999;
}

class InvalidHandlerTypException extends \Exception
{
    public function __construct(FileImportHandler $handler)
    {
       parent::__construct(
          strtr(
            Constants::INVALID_HANDLER_TYPE_EXCEPTION_MESSAGE,
               [
                  ':filetype' => $handler->filetype
               ]
            ),
            Constants::INVALID_HANDLER_TYPE_EXCEPTION_CODE
        );
    }
}

This exception class is immutable - both in its message and exception code. It can only be used when the handler passed in is not an appropriate match for the type of file provided - it cannot be used anywhere else under any other condition, partially because it only accepts a FileImportHandler in a specific method where the FileImportHandler class would be an instance of an object.

This GUARANTEES the appropriate use of the class which can ensure the appropriate HANDLING of the exception as it will only ever be thrown under 1 condition.

Now the usage is simply:

throw new InvalidHandlerTypException($fileImportHandler);

This is clean, verbose, and mitigates the need to revisit this area of code at a later date for better handling of that particular issue. Take the extra couple of minutes to create the custom exception and you will save thousands of company dollars in handling a bug ticket in the future.

Multiple Language Support for Custom Exception Classes

The short answer is NO. The exception message should be based on default the language of the development team - in Elentra's case that's English. This is because the exception message is not normally intended to be sent to the browser - it meant for logging, whether to a file system or database, or for embedding in an Elentra admin email notification (or SMS message).

Let the called endpoint's response object determine the message to send to the client.

In some cases it is ok to send the exception message since it will not be displayed to the user (but viewable in the response object behind the scenes).

eg:

try {
    //some code here
} catch (InvalidHandlerTypException $exception) {
    return (new EnvelopedResponse)
        ->setStatus($exception->getCode())
        ->setData($exception->getMessage());
}

In other cases the business requirements of the ticket may specify that a language appropriate response must be sent:

public const INVALID_HANDLER_TYPE_EXCEPTION_KEY = 'INVALID_HANDLER_TYPE_EXCEPTION'; 

...

try {
    //some code here
} catch (InvalidHandlerTypException $exception) {
    return (new EnvelopedResponse)
        ->setStatus($exception->getCode())
        ->setData(_translate(self::INVALID_HANDLER_TYPE_EXCEPTION_KEY));
}

It is not up to the developer to determine when to send a language specific message or not - this is the decision of the Implementation Lead or Architect that will add it to the technical requirements (I.L.) or technical specifications (Architect) of the ticket description.

If it is not specified, then it is the developer's responsibility to take ownership of this question and ask - do not assume and do not ignore this approach for lack of documentation.

Generic Exceptions

The only time a generic exception should be considered is when the developer is unable to determine what may possible exception may occur (on rare occasions) and does not want the Laravel system to handle the exception. Laravel should never be relied upon for catching/handling errors. When that occurs it is an indication that a use/case (condition) was missed during the development process and an appropriate method of handling the exception should then be added.

This is to avoid responses to the client that appear like this:

{
    'status': 500 ',
    message': 'SQL Exception: Column 'filetype' not found in file_imports table' with Query 'insert into import_files (id, file_name, file_type) values (...) 
}

This is not only unprofessional it's not secure. Do not leave exception output nor debug output in your console nor your response objects.

Last updated