Skip to content

Errors

Lesson 4.3

C has no exceptions. When an API call can fail, it tells you with its return value, and it is your job to look. This lesson shows the error codes and good ways to handle them.

cl_result

Most functions return a cl_result, a number:

  • CL_OK (0) means it worked.
  • Anything else is an error code. They are all negative and start with CL_E_.
cl_result r = cl_easy_give(player, "minecraft:diamon", 1);
if (r != CL_OK) {
    CL_LOGF(CL_LOG_WARN, "give failed with %d", r);
}
[my_mod] give failed with -6

-6 is CL_E_NOT_FOUND: there is no item called minecraft:diamon.

The codes you will see most

Code Number Usually means
CL_E_INVALID_ARGUMENT -2 A wrong value: an empty id, a number out of range, a missing callback.
CL_E_INVALID_HANDLE -3 A handle of the wrong kind, or one that never existed.
CL_E_STALE_HANDLE -4 The thing is gone: the player left, the entity died.
CL_E_NOT_FOUND -6 An item, block, event or option that doesn’t exist (often a typo).
CL_E_ALREADY_EXISTS -7 You registered the same id or command twice.
CL_E_WRONG_PHASE -8 Too early or too late, like registering an item after startup.
CL_E_WRONG_SIDE -10 A server function on the client, or the other way round.
CL_E_PERMISSION_DENIED -11 A missing capability, or another mod’s namespace.
CL_E_BUFFER_TOO_SMALL -12 Your string buffer was too short (see Handles and strings).
CL_E_NOT_LOADED -14 A block in a part of the world that isn’t loaded.
CL_E_LIMIT -18 Something is too big, like a payload over its size limit.

The full list is in clight.h, right at the top. A handy helper for your log lines:

static const char* err_name(cl_result r) {
    switch (r) {
        case CL_OK: return "ok";
        case CL_E_INVALID_ARGUMENT: return "invalid argument";
        case CL_E_STALE_HANDLE: return "stale handle";
        case CL_E_NOT_FOUND: return "not found";
        case CL_E_WRONG_PHASE: return "wrong phase";
        case CL_E_WRONG_SIDE: return "wrong side";
        case CL_E_PERMISSION_DENIED: return "permission denied";
        case CL_E_NOT_LOADED: return "not loaded";
        default: return "error";
    }
}

The last error and its message

Not every function can return a cl_result. cl_living_health returns the health as a float, and cl_stack_create_by_name returns a stack. When those fail, they return an empty value (0, false, a null handle) and remember the reason. cl_last_error() gives you that reason:

cl_item_stack s = cl_stack_create_by_name(CL_STR("my_mod:rubby"), 1);
if (CL_IS_NULL(s)) {
    CL_LOGF(CL_LOG_WARN, "no stack: %d", cl_last_error());
}

Many errors also come with a short message. cl_last_error_message gets it, with the usual buffer pattern:

char why[256];
size_t len = 0;
cl_last_error_message(why, sizeof why, &len);
CL_LOGF(CL_LOG_WARN, "failed: %s", why);

The last error belongs to the current thread and is overwritten by the next failing call, so read it right away.

Development mode helps

When you start the game with clight run, it runs in development mode. Then many misuses are logged by the game itself, with the function’s name, even if your code doesn’t check:

[clight] [my_mod] cl_level_set_block failed with -10

That catches a lot of mistakes early. Players don’t run in development mode, so your code should still check the results that matter.

A simple habit

You don’t have to check every single call. A good rule:

  • Always check registrations and subscriptions in mod_main and lifecycle.register. If they fail, your mod is broken, and returning the error from mod_main makes the game say so.
  • Check calls whose result you use: a lookup, a position, a name.
  • Don’t bother with calls where failing does no harm, like sending a message to a player who just left.

In mod_main, chain your setup and stop at the first failure:

static cl_result mod_main(void) {
    cl_result r = CL_OK;
    if (r == CL_OK) r = CL_ON(CL_EV_PLAYER_JOIN, on_player_join);
    if (r == CL_OK) r = CL_ON(CL_EV_BLOCK_BREAK, on_block_break);
    if (r == CL_OK) r = CL_ON(CL_EV_SERVER_COMMAND_REGISTER, on_command_register);
    return r;
}

Each line only runs if all before it worked, and the first error is what mod_main returns. If it isn’t CL_OK, the game doesn’t load the mod and the log says clight_mod_main returned -6 (or whichever code it was).

Try it

Write a function bool give_or_tell(cl_entity player, const char* item_id, int count) that gives the item, and if that fails, logs a warning with the item id and the error code, sends the player “Sorry, something went wrong.”, and returns false.

Show a solution
static bool give_or_tell(cl_entity player, const char* item_id, int count) {
    cl_result r = cl_easy_give(player, item_id, count);
    if (r == CL_OK) return true;

    CL_LOGF(CL_LOG_WARN, "could not give %s: error %d", item_id, r);
    cl_easy_send_message(player, "Sorry, something went wrong.");
    return false;
}

The player only sees a friendly sentence; the details go into the log, where you look when you fix it.

Recap: functions return CL_OK or a negative CL_E_ code, getters that can’t return a code leave it in cl_last_error(), and mod_main should return the first setup error so the game reports it.