Using exceptions & Try-Catch in OpenSwoole

Latest version: pecl install openswoole-22.1.2 | composer require openswoole/core:22.1.5

Proper use of exceptions & Try/Catch Blocks in OpenSwoole

OpenSwoole enables multiprocessing and concurrency via using multiple processes over multiple CPU cores, and by introducing a new programming model with coroutines to the PHP language. Coroutines being one of the biggest changes and differences when programming with OpenSwoole and PHP. Once you have a basic understanding of how coroutines work and why they increase concurrency, it is easy to understand how we can correctly use PHP exceptions and try/catch blocks; there are some things to consider when handling errors and exceptions when programming with coroutines and multiple processes.

For more information and to learn more about OpenSwoole fibers/coroutines, read through the coroutines documentation first.

Traditional PHP Exception Handling

In PHP, we typically use try/catch blocks to handle errors that are out of the developer's control, usually due to unexpected behavior from external services or unpredictable code.

Since traditional PHP code runs in a stateless model, you can think of a request occupying a process and contained within a process, there is no need to worry about memory conflicts or stateful execution. Traditional PHP creates a new state for every request or script execution.

When developing with OpenSwoole and fibers/coroutines, there are a few things to consider when handling PHP exceptions…

Handling Exceptions in OpenSwoole

With OpenSwoole, you can still work with PHP's exception system, but you must remember to consider a few things first:

  • A try/catch block cannot contain any subsequent calls to go() / OpenSwoole\Coroutine::create()
  • Make sure your containing code within the try/catch does not create further coroutines
  • A try/catch must only be used within a single coroutine
  • You should only use a try/catch when needed, for specific events

The main thing to remember is that a try/catch should only be used within a single coroutine. You can think of a coroutine like a single process, a try/catch cannot be used in a way that spans across multiple coroutines (or "processes"). This is because coroutines run independently, in a different context to where the try/catch is placed, so it would never catch any exceptions thrown.

Incorrect Try/Catch Usage

<?php

try {
    go(function () {
        throw new \RuntimeException(__FILE__, __LINE__);
    });
} catch (\Throwable $e) {
    echo $e;
}

The example above is wrong because it creates a new coroutine context within the try block, this is not good because any errors inside the coroutine will not be caught as the call to go() returns right away and continues the execution. You must throw and catch errors within the same coroutine, not across coroutines. Once the coroutine is created and during the time it exists, a PHP fatal error of Uncaught RuntimeException will be thrown and will not be caught as expected.

Correct Try/Catch Usage

<?php

function test()
{
    throw new \RuntimeException(__FILE__, __LINE__);
}

go(function () {
    try {
        test();
    } catch (\Throwable $e) {
        echo $e;
    }
});

The correct example above shows us how we can use the try block and catch errors in the same coroutine, just like how you would with traditional PHP, in a single process, to make things simpler, you can think of coroutines like userland threads, they run in the same process but have their own context so a try/catch across multiple coroutines won't work.

Above, the coroutine is created first and our try/catch block runs entirely inside the coroutine, allowing any exceptions to be caught.

Just make sure you are using a try/catch within the same coroutine if you want to catch any potential exceptions.

Advice on Using Exceptions

We rely on try/catch blocks to handle errors when we don't know what the expected outcome is, usually when contacting external services, but most of the time we can perform other checks with conditional statements or contact other functions to prevent an error from being thrown.

However, it is not always the case, and sometimes it is just best to wrap our code in a try/catch block. But we shouldn't do this all the time, only when necessary, the point of catching an error is to do something sensible when that error occurs, some form of recovery is useful.

You shouldn't just rely on try/catch blocks to handle errors, you should first adopt proper programming practices and actively prevent exceptions from being thrown. For example, validating user input in advance or verifying that you have permission to do something beforehand.

Generally, try/catch blocks should be used when:

  • Contacting external services, where our code is not responsible for potential outcomes
  • For specific code areas where you might expect fatal errors because there is no other way to check
  • Needing to handle exceptions in a custom manner by writing your own exception handler
  • Catch exceptions if and only if you have a meaningful way of handling them, not just repeating the error message
Last updated on October 14, 2022