Using exceptions & Try-Catch in Swoole

Proper use of exceptions & Try/Catch Blocks in Swoole

Swoole enables multi-processing 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 Swoole 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 Swoole fibers/coroutines, read through the coroutines documentation first.

Traditional PHP Exception Handling

In PHP we are use to using a Try/Catch block to handle errors that our out of the developers control, usually due to external services or unpredictable code.

Because traditional PHP runs in a stateless model, where you can think of a request taking up one process and contained within one process, there is no need to worry about memory conflicts or stateful execution. Traditional PHP creates a new state for each request or execution of a script.

When developing with Swoole and fibers/coroutines there are a few things to consider when working with PHP exception handling...

Using Exception Handling in Swoole

With Swoole you can still use and 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() / Swoole\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 a try/catch should only be used withing 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 coroutine or "processes". This is because coroutines will 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 on with execution. You must catch and throw 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 more simpler, you can think of coroutines like user-land 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 that is created, 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 rely on try/catches to handle errors, you should adopt proper programming practices first and actively prevent exceptions being thrown, for example, checking user input beforehand or if you have permission to do something prior.

Generally, try/catch blocks should be used when:

  • Contacting external services, where our code is not responsible for potential outcomes
  • For specific areas of code where you might expect a fatal error 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