Aoik

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:

Reply to: