Lua error handling mechanism
Lua version 5.3.5.
Lua's execution flow has been introduced in this post.
In Lua, invalid operations like performing arithmetic operation on nil
will cause an error. Arbitrary errors can be raised in user code by calling function
error
.
The Lua language does not provide syntax for catching errors. To catch errors
without aborting the execution, user code should use pcall
or xpcall
to
call a target function that may cause errors:
pcall(target_func)
xpcall(target_func, error_handler)
Internally, pcall
in user code corresponds to lbaselib.c--luaB_pcall
. In
lbaselib.c--luaB_pcall
, a chain of calls happens: lapi.c--lua_pcallk
-> ldo.c--luaD_pcall
-> ldo.c--luaD_rawrunprotected
:
int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) {
// Save old number of nexted C calls
unsigned short oldnCcalls = L->nCcalls;
// Create new error jump info
struct lua_longjmp lj;
// Set initial status
lj.status = LUA_OK;
// Save old error jump info
lj.previous = L->errorJmp; /* chain new error handler */
// Set new error jump info
L->errorJmp = &lj;
// `LUAI_TRY` calls `setjmp` to save current execution context to
// `L->errorJmp->b`, so that in case of an error, execution can jump back by
// restoring context in `L->errorJmp->b`. `LUAI_TRY` calls `(*f)(L, ud)`.
LUAI_TRY(L, &lj,
(*f)(L, ud);
);
// Restore old error jump info
L->errorJmp = lj.previous; /* restore old error handler */
// Restore old number of nexted C calls
L->nCcalls = oldnCcalls;
// Return call status
return lj.status;
}
In ldo.c--luaD_rawrunprotected
, it replaces L->errorJmp
with a new
lua_longjmp
struct (the old one can still be found via L->errorJmp.previous
):
#define luai_jmpbuf jmp_buf
struct lua_longjmp {
struct lua_longjmp *previous;
luai_jmpbuf b;
volatile int status; /* error code */
};
Then ldo.c--LUAI_TRY
is called with the new lua_longjmp
struct.
#define LUAI_TRY(L,c,a) if (setjmp((c)->b) == 0) { a }
c
is a pointer to the new lua_longjmp
struct. a
is the statement that
calls the target function corresponding to the argument to pcall
in user
code.
setjmp
stores the current execution context in (c)->b
(i.e.
L->errorJmp.b
), then returns 0. If it returns 0, the statement a
that calls the target function will run. The case it returns non-zero will be talked about later.
The target function runs until an error occurs.
Internally, when an error occurs, ldebug.c--luaG_runerror
is called:
l_noret luaG_runerror (lua_State *L, const char *fmt, ...) {
CallInfo *ci = L->ci;
const char *msg;
va_list argp;
luaC_checkGC(L); /* error message uses memory */
va_start(argp, fmt);
msg = luaO_pushvfstring(L, fmt, argp); /* format message */
va_end(argp);
if (isLua(ci)) /* if Lua function, add source:line information */
luaG_addinfo(L, msg, ci_func(ci)->p->source, currentline(ci));
luaG_errormsg(L);
}
ldebug.c--luaG_runerror
pushes an error message to stack, then calls
ldebug.c--luaG_errormsg
. (The error
function in user code corresponds to
lbaselib.c--luaB_error
, which calls lapi.c--lua_error
, which calls
ldebug.c--luaG_errormsg
too.)
l_noret luaG_errormsg (lua_State *L) {
if (L->errfunc != 0) { /* is there an error handling function? */
// Get error function's TValue pointer.
StkId errfunc = restorestack(L, L->errfunc);
// Put error message in `L->top`.
setobjs2s(L, L->top, L->top - 1); /* move argument */
// Put error function's TValue in `L->top - 1`.
setobjs2s(L, L->top - 1, errfunc); /* push function */
L->top++; /* assume EXTRA_STACK */
// Call the error function
luaD_callnoyield(L, L->top - 2, 1); /* call it */
}
luaD_throw(L, LUA_ERRRUN);
}
In ldebug.c--luaG_errormsg
, if an error handler is present in L->errfunc
,
it is called. Then ldo.c--luaD_throw
is called:
l_noret luaD_throw (lua_State *L, int errcode) {
if (L->errorJmp) { /* thread has an error handler? */
L->errorJmp->status = errcode; /* set status */
LUAI_THROW(L, L->errorJmp); /* jump to it */
}
else { /* thread has no error handler */
global_State *g = G(L);
L->status = cast_byte(errcode); /* mark it as dead */
if (g->mainthread->errorJmp) { /* main thread has a handler? */
setobjs2s(L, g->mainthread->top++, L->top - 1); /* copy error obj. */
luaD_throw(g->mainthread, errcode); /* re-throw in main thread */
}
else { /* no handler at all; abort */
if (g->panic) { /* panic function? */
seterrorobj(L, errcode, L->top); /* assume EXTRA_STACK */
if (L->ci->top < L->top)
L->ci->top = L->top; /* pushing msg. can break this invariant */
lua_unlock(L);
g->panic(L); /* call panic function (last chance to jump out) */
}
abort();
}
}
}
In ldo.c--luaD_throw
, if L->errorJmp
is present (recall L->errorJmp
is
set with a new lua_longjmp
struct in ldo.c--luaD_rawrunprotected
), the
error code will be stored in L->errorJmp->status
. Then ldo.c--LUAI_THROW
is
called with L->errorJmp
:
#define LUAI_THROW(L,c) longjmp((c)->b, 1)
In ldo.c--LUAI_THROW
, longjmp
is called with the execution context stored
in L->errorJmp.b
. Now the execution flow jumps back to where the
corresponding setjmp
that stored the execution context in L->errorJmp.b
was called:
#define LUAI_TRY(L,c,a) if (setjmp((c)->b) == 0) { a }
Notice the second argument of longjmp
is 1, which will become the return
value of the setjmp
. Now that setjmp
returns non-zero, the statement a
will not run.
In ldo.c--luaD_rawrunprotected
, after LUAI_TRY
is done, the old
lua_longjmp
struct pointed to by L->errorJmp.previous
is restored as
L->errorJmp
. The status code stored in L->errorJmp->status
during the
target function call is used as the return value (recall the error code is
stored in it by ldo.c--luaD_throw
).
In lbaselib.c--luaB_pcall
, the chain of calls lapi.c--lua_pcallk
->
ldo.c--luaD_pcall
-> ldo.c--luaD_rawrunprotected
returns. The status code
is returned. Then lbaselib.c--finishpcall
is called:
static int finishpcall (lua_State *L, int status, lua_KContext extra) {
if (status != LUA_OK && status != LUA_YIELD) { /* error? */
lua_pushboolean(L, 0); /* first result (false) */
lua_pushvalue(L, -2); /* error message */
return 2; /* return false, msg */
}
else
return lua_gettop(L) - (int)extra; /* return all results */
}
If the status code indicates an error, the first return value of pcall
in
user code is set to be false
, the second return value is set to be the error
message pushed to stack by ldebug.c--luaG_runerror
.
If the status code indicates no error, the first return value of pcall
in
user code is set to be true
(already pushed to stack in
lbaselib.c--luaB_pcall
), followed by return values from the target
function called.
In summary, error handling is implemented by storing execution context using
setjump
, and jumping back using longjump
in case of an error. Before
jumping back, an error message is pushed to stack, and the error code is stored
in L->errorJmp.status
. According to whether the status code indicates an
error, pcall
or xpcall
returns either false
or true
as the first return
value.
Comments: