Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get true stack trace of an error in lua pcall

Tags:

lua

So for my pcall statements, I've been doing something like this

local status, err = pcall(fn)
if not status then
     print(err)
     print(debug.stacktrace())
end

This works fine for some basic stuff but the issue is that debug.stacktrace() returns the CURRENT relative stack trace, not the stack trace of the error. If the error within fn happened 10 levels down in the stack, then I wouldn't know where exactly it occurred, just that this pcall block is failing. I was wondering if there was a way to get the stack trace of the pcall and not the current stack trace. I tried debug.stacktrace(err) but it didn't make a difference.

like image 307
Arhowk Avatar asked Aug 21 '17 02:08

Arhowk


Video Answer


2 Answers

You need to use xpcall to provide a custom function that will add the stacktrace to the error message. From PiL:

Frequently, when an error happens, we want more debug information than only the location where the error occurred. At least, we want a traceback, showing the complete stack of calls leading to the error. When pcall returns its error message, it destroys part of the stack (the part that went from it to the error point). Consequently, if we want a traceback, we must build it before pcall returns. To do that, Lua provides the xpcall function. Besides the function to be called, it receives a second argument, an error handler function. In case of errors, Lua calls that error handler before the stack unwinds, so that it can use the debug library to gather any extra information it wants about the error.

You may want to check this patch that extends pcall to include stacktrace.

As suggested in the comments, you can use local ok, res = xpcall(f, debug.traceback, args...) with Lua 5.2+ or LuaJIT (with Lua 5.2 compatibility turned on) and used a patch mentioned above for Lua 5.1.

like image 106
Paul Kulchenko Avatar answered Oct 24 '22 14:10

Paul Kulchenko


The basic problem is (roughly) that pcall must unwind the stack so that your error handling code is reached. This gives two obvious ways to tackle the problem: Create the stack trace before unwinding, or move the (potentially) error-throwing code out of the way, so the stack frames don't have to be removed.

The first is handled by xpcall. This sets an error handler that can create a message while the stack is still intact. (Note that there are some situations where xpcall will not call the handler,1 so it's not suitable for cleanup code! But for stack traces, it's generally good enough.)

The second option (this works always2) is to preserve the stack by moving the code to a different coroutine. Instead of

local ok, r1, r2, etc = pcall( f, ... )

do

local co = coroutine.create( f )
local ok, r1, r2, etc = coroutine.resume( f, ... )

and now the stack (in co) is still preserved and can be queried by debug.traceback( co ) or other debug functions.

If you want the full stack trace, you'll then have to collect both the stack trace inside the coroutine and the stack trace outside of it (where you currently are) and then combine both while dropping the first line of the latter:

local full_tb = debug.traceback( co )
             .. debug.traceback( ):sub( 17 ) -- drop 'stack traceback:' line

1 One situation in which the handler isn't called is for OOMs:

g = ("a"):rep( 1024*1024*1024 ) -- a gigabyte of 'a's
-- fail() tries to create a 32GB string – make it larger if that doesn't OOM
fail = load( "return "..("g"):rep( 32, ".." ), "(replicator)" )

-- plain call errors without traceback
fail()
--> not enough memory

-- xpcall does not call the handler either:
xpcall( fail, function(...) print( "handler:", ... ) return ... end, "foo" )
--> false   not enough memory

-- (for comparison: here, the handler is called)
xpcall( error, function(...) print( "handler:", ... ) return ... end, "foo" )
--> handler: foo
--  false   foo

-- coroutine preserves the stack anyway:
do
   local co = coroutine.create( fail )
   print( "result:", coroutine.resume( fail ) )
   print( debug.traceback( co ) .. debug.traceback( ):sub( 17 ) )
end
--> result: false   not enough memory
--> stack traceback:
--    [string "(replicator)"]:1: in function 'fail'
--    stdin:4: in main chunk
--    [C]: in ?

2 Well, at least as long as Lua itself doesn't crash.

like image 36
nobody Avatar answered Oct 24 '22 14:10

nobody