Error Handling

Luna SDK does not use the exception mechanism provided by C++. Instead, it adopts a more light-weight error-handling mechanism by returning error codes. Compared to other error-code based solutions, Luna SDK manages error codes and the relationship between error codes, so the user can extend the error handling mechanism easily.

Error code

#include <Luna/Runtime/Error.hpp>

ErrCode represents one error code, which is a machine-sized unsigned integer (usize). ErrCode is defined as a dedicated structure type to distinguish from normal return values, the actual error code value can be fetched by code property of ErrCode. We use error code 0 to represent a successful operation (no error), and any non-zero error code value represents one error.

The error code value is not defined directly. Instead, the user should call get_error_code_by_name to fetch the error code for one specific error. The error code is generated by the system on the first call to get_error_code_by_name, and is cached and returned directly on succeeding calls to get_error_code_by_name with the same arguments. The error code for the same error will change in different processes, so do not store the error code directly, store its name and category (which will be explained in the following section) instead.

Error name and category

#include <Luna/Runtime/Error.hpp>

Every ErrCode is described by two properties: error name and error category, which is required when calling get_error_code_by_name, and can be fetched by get_error_code_name and get_error_code_category. Error name is a UTF-8 string that briefly describes the error, while error category is used to hold one set of error codes in the same domain. For example, the Runtime module of Luna SDK defines one error category called BasicError, which contains error codes like bad_arguments, out_of_memory, not_supported, etc.

The error category is represented by errcat_t, and is identified by one UTF-8 name string. You can get errcat_t from its name by calling get_error_category_by_name, and get the name of one errcat_t by calling get_error_category_name. Error categories can also contain sub-categories, for example, BasicError may contains one IOError sub-category that contains all error codes related to IO errors. In such case, the error category name and sub-category name should both be specified for sub-categories, separated by double colons (::), like BasicError::IOError.

You can call get_all_error_codes_of_category to get all error codes of one specific error category, and get_all_error_subcategories_of_category to get all error sub-categories of one specific error category.

Declaring error codes

Error codes can be declared by specifying error categories as namespaces, and error codes as functions that return the corresponding ErrCode instances. All error categories should be declared directly in Luna namespace. Every error category should have one errtype function that returns the errcat_t instance of the specified error category.

namespace Luna
{
    namespace MyError
    {
        //! Gets the error category object of `MyError`.
        LUNA_MYMODULE_API errcat_t errtype();

        LUNA_MYMODULE_API ErrCode my_error_1();
        LUNA_MYMODULE_API ErrCode my_error_2();
        LUNA_MYMODULE_API ErrCode my_error_3();
        //...
        namespace MySubError
        {
            //! Gets the error category object of `MySubError`.
            LUNA_MYMODULE_API errcat_t errtype();

            LUNA_MYMODULE_API ErrCode my_error_4();
            LUNA_MYMODULE_API ErrCode my_error_5();
            //...
        }
    }
}

When implementing such functions, you may use static local variables to prevent fetching the error code every time it is called:

namespace Luna
{
    namespace MyError
    {
        LUNA_MYMODULE_API errcat_t errtype()
        {
            static errcat_t e = get_error_category_by_name("MyError");
            return e;
        }
        LUNA_MYMODULE_API ErrCode my_error_1();
        {
            static ErrCode e = get_error_code_by_name("MyError", "my_error_1");
            return e;
        }
        //...
        namespace MySubError
        {
            LUNA_MYMODULE_API errcat_t errtype()
            {
                static errcat_t e = get_error_category_by_name("MyError::MySubError");
                return e;
            }
            LUNA_MYMODULE_API ErrCode my_error_4()
            {
                 static ErrCode e = get_error_code_by_name("MyError::MySubError", "my_error_4");
                return e;
            }
            //...
        }
    }
}

Built-in errors

Runtime/Error.hpp contains a list of error codes that covers most common error types, like bad_arguments, bad_platform_call, out_of_memory, not_found, already_exists, etc. All these error codes are declared in BasicError error category, and can be used directly.

Besides error codes in BasicError , some built-in modules of Luna SDK declare their own error codes. For example, RHI module declares device_lost in RHIError error category to indicate one graphic device removal error. You can check module documentations and interface files for error codes defined by such modules.

Result object

#include <Luna/Runtime/Result.hpp>

To represent one function that may throw errors, you should wrap the return type of the function with the result object typeR<T> , which encapsulates the returned value of the function as well as one error code. The result object can be constructed by passing normal return values (which indicates a successful function call) or error codes (which indicates one error). If the result object is constructed by error, its result object will not be initialized.

The following example shows how to declare and implement one function that may throw errors:

R<u64> get_file_size(File* file)
{
    u64 size;
    BOOL succeeded = system_get_file_size(file, &size);
    if(succeeded) return size; // Return the return value means success.
    else return BasicError::bad_platform_call(); // Return the error code means failure.
}

If the return type of the function is R<void>, you can return ok to indicate one successful function call. Note that using ok is allowed only if the function return value is R<void>. You can also use RV to replace of R<void> for convenience.

RV reset_file_cursor(File* file)
{
    BOOL succeeded = system_reset_file_cursor(file);
    if(succeeded) return ok;
    else return BasicError::bad_platform_call();
}

On the caller side, we can use succeeded and failed to test whether one result object represents one valid return value or one error code:

auto res = reset_file_cursor(file);
if(failed(res))
{
    // Gets the error code stored in `R<T>`.
    ErrCode err = res.errcode();
    // Handle the error.
    // ...
}

Error objects

#include <Luna/Runtime/Error.hpp>

Error codes indicate only the type of the error, without any further information, which can be inconvenient for the user to indicating the error. For such purpose, Luna SDK provides error objects that extend error codes to provide more detailed information about the error.

One error object is represented by Error and contains three members:

  1. code: The error code.
  2. message: One UTF-8 short description of the error.
  3. info: One Variant that may contain any additional error information provided.

To return one error object instead of one error code, first set the error object by calling get_error, then returns BasicError::error_object as the returned error code of the function:

Error& err = get_error();
err = Error(BasicError::not_found(), "The specified file %s is not found.", file_name);
return BasicError::error_object();

The error object fetched by get_error is a thread-local object attached to the current thread, so error objects in different threads are independent to each other. If you want to pass error objects between different threads, you can always store one Error instance down and pass it using your own methods.

You can also use set_error to simplify the process of creating and returning error objects. The above code can be rewritten by:

return set_error(BasicError::not_found(), "The specified file %s is not found.", file_name);

set_error always returns BasicError::error_object, so we can return it directly.

On the caller side, if we find the error code of one function is BasicError::error_object, we should retrieve the real error code by checking the same error object set by the calling function:

auto res = do_something();
if(failed(res))
{
    ErrCode err = res.errcode();
    if(err == BasicError::error_object())
    {
        err = get_error().code;
    }
    // Handle the error.
    // ...
}

We can use unwrap_errcode to simplify this process and retrieve the error code directly like so:

auto res = do_something();
if(failed(res))
{
    ErrCode err = unwrap_errcode(res);
    // Handle the error.
    // ...
}

unwrap_errcode will retrieve the error code from R<T> result object, and if the error code is BasicError::error_object, it will then retrieve the real error code automatically from the error object of this thread.

We can also call explain to fetch the message stored on the error object if the error code is BasicError::error_object, or the name of the error code if not:

auto res = do_something();
if(failed(res))
{
    debug_printf("%s", explain(res.errcode()));
}

Try-catch macros for error handling

#include <Luna/Runtime/Result.hpp>

Correctly handling functions that may throw errors requires a lot of if statements to judge whether every function call is successful, which takes a lot of effort. In order to ease this, Luna SDK provides macros that can be used to handle throwable functions using a try-catch syntax, much like those in C++.

In order to catch error codes returned by throwable function, we firstly need to declare one pair of try-catch blocks using lutry and lucatch like so:

lutry
{

}
lucatch
{

}

lutry block is the place where throwable functions are called. In this block, throwable functions are wrapped by luexp, lulet and luset macros:

  1. luexp is used if the return type of the function is R<void>.
  2. lulet creates a new local variable to hold the return value of the function.
  3. luset assigns the return value of the function to one existing variable.

The user can also use luthrow to throw one directly. The following code shows the usage of these four macros:

lutry
{
    luexp(do_something_that_may_fail());
    lulet(size, get_file_size(file)); // Creates one new local variable `size`.
    luexp(set_file_size(file, 1024));
    u64 new_size;
    luset(new_size, get_file_size(file)); // Assigns to one existing variable `new_size`.
    if(new_size != 1024)
    {
        luthrow(BasicError::bad_platform_call()); // Throw errors directly.
    }
}
lucatch
{
    //...
}

For all these four macros, if the calling function or luthrow throws errors, the execution flow will be interrupted and redirected to lucatch block by a internal goto jump. In lucatch block, the user should handle the error, or just return the error to the caller function. lures macro is used in this block to represent the error code.

lucatch
{
    ErrCode code = unwrap_errcode(lures); // To fetch the real error code if lures is `BasicError::error_object`.
    if(code == BasicError::bad_platform_call())
    {
        // Do something...
    }
    else if(code == BasicError::bad_arguments())
    {
        // Do something...
    }
    else return lures; // Forward the error to caller function if the error cannot be handled here.
}

If the user does not want to handle errors at all, she can use lucatchret instead of lucatch block, which will forward all errors caught to the caller function:

lutry
{
    //...
}
lucatchret; // Return all errors caught.

In most cases, only one lutry-lucatch pair is needed for one function. If you need multiple lutry-lucatch pairs in the same function, add suffix numbers to macros of succeeding lutry-lucatch pairs after the first pair like so:

RV func()
{
    // First pair.
    lutry
    {
        luexp(...);
        lulet(a, ...);
        luset(a, ...);
        luthrow(...);
    }
    lucatch
    {
        return lures;
    }
    // Another pair.
    lutry2
    {
        luexp2(...);
        lulet2(a, ...);
        luset2(a, ...);
        luthrow2(...);
    }
    lucatch2
    {
        return lures2;
    }
    // Another pair.
    lutry3
    {
        luexp3(...);
        lulet3(a, ...);
        luset3(a, ...);
        luthrow3(...);
    }
    lucatch3
    {
        return lures3;
    }
}