Exception Handling

Default behavior

One of the main benefits of using contexts is their “unroll” feature which works even when an exception occurs in a user-provided callback. This means, that exitContext() is invoked, even if the user’s code execution gets interrupted by an exception. To illustrate this, we’ll slightly modify the example from the section named Multiple context arguments. We’ll use same MyInt objects as context managers for all context arguments

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class MyInt implements ContextManagerInterface
{
    public $value;

    public function __construct(int $value)
    {
        $this->value = $value;
    }

    public function enterContext()
    {
        echo "enter: " . $this->value . "\n";
        return $this->value;
    }

    public function exitContext(?\Throwable $exception = null) : bool
    {
        echo "exit: " . $this->value . "\n";
        return false;
    }
}

Instead of doing anything useful, we’ll just throw our custom exception MyException from the context (later):

1
2
3
class MyException extends Exception
{
}

The exception handling and unroll process may be demonstrated with the following snippet. We expect all the values 1, 2, and 3 to be printed at enter and the same numbers in reversed order printed when context exits. Finally, we should also receive MyException.

1
2
3
4
5
6
7
8
try {
    with(new MyInt(1), new MyInt(2), new MyInt(3))(function (int ...$args) {
        throw new MyException('my error message');
    });
} catch (MyException $e) {
    fprintf(STDERR, "%s\n", $e->getMessage());
    exit(1);
}

The outputs from above snippet shall be

  • stdout:

    1
    2
    3
    4
    5
    6
    enter: 1
    enter: 2
    enter: 3
    exit: 3
    exit: 2
    exit: 1
    
  • stderr:

    1
    my error message
    

Handling exceptions in exitContext

If one of the context managers returns true from its exitContext(), all the remaining context managers will receive null as $exception argument and the exception will be treated as handled (it will not be propagated to the context caller). To demonstrate this, let’s consider the following modified MyInt class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyInt implements ContextManagerInterface
{
    public $value;
    public $handle;

    public function __construct(int $value, bool $handle = false)
    {
        $this->value = $value;
        $this->handle = $handle;
    }

    public function enterContext()
    {
        echo "enter: " . $this->value . "\n";
        return $this->value;
    }

    public function exitContext(?\Throwable $exception = null) : bool
    {
        echo "exit: " . $this->value . " (" . strtolower(gettype($exception)) . ")\n";
        return $this->handle;
    }
}

The object may be configured to return true or false. What happens when one of the context managers returns true, may be explained by the following snippet

1
2
3
with(new MyInt(1), new MyInt(2), new MyInt(3, true), new MyInt(4))(function (int ...$args) {
    throw new MyException('my error message');
});

When unrolling, the objects MyInt(4) and MyInt(3, true) receive an instance of MyException as $exception, then MyInt(3, true) returns true and the remaining objects MyInt(2) and MyInt(1) receive null as $exception. The exception thrown from user-provided callback is not propagated to the outside. The code from the above snippet runs without an exception and outputs the following text

1
2
3
4
5
6
7
8
enter: 1
enter: 2
enter: 3
enter: 4
exit: 4 (object)
exit: 3 (object)
exit: 2 (null)
exit: 1 (null)