Incomplete stack trace

I’ve been working on a game for some time now, and something has been bugging me from the start. I’ve been using coroutine.wrap() for creating new coroutines, which is nice because I can pass arguments to the function and have the error propagate (I actually want the error to propagate). Sometimes, however, it shows a very weird behavior: if the function the coroutine is executing errors, it shows the stack trace only up to the point where coroutine.wrap() was called, and no further inside the function called (except the exact point where the error happened), which makes debugging hard. Has anyone else had this problem? Is this the intended behavior? Does anyone know any way around it?

Convoluted but functional repro:

function a()
	m()
end
function b()
	a()
end
function c()
	b()
end
function d()
	c()
end
coroutine.wrap(d)()
1 Like

That’s because a stacktrace can only be made for the current callstack (which is linked to a thread).
When you do coroutine.wrap(d)(), the function d runs in a separate thread.
It’s just that if it errors without yielding, coroutine.wrap itself rethrows the error.

It’s basicly this:

-- Need this to keep trailing nils
local function Tuple(...)
	return {n=select("#",...),...}
end
local function wrap(func)
	local th = coroutine.create(func)
	return function(...)
		local result = Tuple(coroutine.resume(th,...))
		if result[1] then
			return unpack(result,2,result.n)
		end error(result[2],2)
	end
end

You can work around this by calling wait() inside d:

local function a()
	error"hi"
end
local function b()
	wait()
	error"hi"
end
coroutine.wrap(a)() -- This would actually error as your code does
-- (Actually the code below doesn't run because the line above errored, but eh)
coroutine.wrap(b)() -- This call itself would return nothing, not even error
-- A tick later your output will suddenly show the error"hi" with full stacktrace
-- (Well, "full stacktrace" doesn't include coroutine.wrap(b)() or higher, so eh)

If you NEED the stacktrace, and your code doesn’t yield (so no stacktrace in the output), use xpcall:

local function a()
	error"hi"
end
local function b()
	a()
end
print(xpcall(a,function(e) print(debug.traceback()) return e end))
--[[ Output
	-- This part is printed from debug.traceback()
Script 'Workspace.Script', Line 7
Script 'Workspace.Script', Line 2
Script 'Workspace.Script', Line 7
Stack End
	-- This is what xpcall returned: false, "Workspace.Script:2: hi"
false Workspace.Script:2: hi
]]
-- You could also do:
print(xpcall(a,debug.traceback))
-- xpcall returns: false, "STACKTRACE"
--[[ Output
false Script 'Workspace.Script', Line 2
Script 'Workspace.Script', Line 7
Stack End
]]

Mind that if you use xpcall, you can’t yield anywhere inside the functions.

You can’t combine xpcall with coroutines to get stacktraces in yielding functions.
I’ve tried that a long time ago, before I came to the conclusion it’s impossible.
(And when I say impossible when it’s related to (RBX.)Lua, it’s really impossible)

1 Like

If you want to get a correct stacktrace from Lua, try something like this:

function wrap(f)
	local c = coroutine.create(f)
	return function(...)
		local returns = {coroutine.resume(c)}
		local succ = table.remove(returns, 1)
		local err = table.remove(returns, 1)
		
		if not succ then
			local traceback = debug.traceback(c)
			return succ, err, traceback
		end
		
		return unpack(returns)
	end
end

However, keep in mind that not everything will work exactly the same once in a coroutine.

1 Like

You surprised me with debug.traceback(routine) there… which doesn’t exist.
(I actually had to test that in studio to make sure, which I just did)
y u tell them lies and giv them false hope

Also, you should use a Tuple function, because everyone cares about trailing nils.

It should exist.

We both know this is ROBLOX.
They probably just linked it to the same thing used during an error.
(Although it would be nice if it worked with threads. Makes making a haxy SB much easier)