I have been using and struggling with coroutine.wrap for several years now due to issues with stack traces from errors occurring within them. Sometimes you get no errors, incomplete stack traces, or the stack trace starting from the line the coroutine is started on rather than the line the error occurred on. You could use Spawn as an alternative to coroutines however it doesn’t run immediately.
This lead me to about 2 hours ago, when for the 3rd time I checked to see if yielding within xpcall had been enabled yet (it has) and set to work to see if it was possible to fix this (it was).
From my tests this method is as fast as directly using coroutine.wrap.
The functions
You can get/require the following model (It returns a table containing CoroutineWithStack and ResumeWithStack at named keys, access via require(CoroutineErrorHandling).CoroutineWithStack or .ResumeWithStack):
Note: If you want to require by ID and use it on the client you just need to require the ID from the server and then on the client do local CoroutineErrorHandling = require(game:GetService("ReplicatedStorage"):WaitForChild("CoroutineErrorHandling"))
The code is also up on github:
Simply pass the function and arguments you want to run in the coroutine to the CoroutineWithStack
function and it’ll run it with support for full stack traces if an error occurs.
Drawbacks
Stack trace is in the error message
Due to how this works, there is a second stack trace included as you will see in blue in the images below. Unfortunately I can’t actually set the stack using error() so it includes the stack from that point with the actual stack in the error message (the red).
coroutine.resume(Thread) swallowing the error
coroutine.resume acts like pcall and returns the error instead of outputting it. You can either handle this resulting error yourself or if you still want it to output as an error you can use one of the following solutions:
-
If you want the stack for errors to trace back to the last resume instead of the point the thread was initially created you can use
ResumeWithStack
instead of coroutine.resume. -
If you want the stack to trace back to when the thread first start just use coroutine.resume normally and if it errors just use error() to output it, like so
local Ran, Error = coroutine.resume(Thread)
if not Ran then
error(Error, 0)
end
Examples
Example 1
Lets say I want to run the following code:
local CoroutineWithStack = require(4851605998)
local function Add(a, b)
return a + b
end
local function Func(a, b)
wait(2)
return Add(a, b)
end
local function DoThing()
coroutine.wrap(Func)(1, "b")
end
DoThing()
With normal coroutine.wrap we get the following error:
As you can see, it has missed the line which started the coroutine from the stack (line 11) and the line that called the DoThing() function (line 13)
If we replace coroutine.wrap(Func)(Args) with CoroutineWithStack(Func, Args) we get the following error
Modified code
local CoroutineWithStack = require(4851605998)
local function Add(a, b)
return a + b
end
local function Func(a, b)
wait(2)
return Add(a, b)
end
local function DoThing()
CoroutineWithStack(Func, 1, "b")
end
DoThing()
It now includes the full stack right up to line 13 (As I said before, the blue stack is the stack from the point I actually call error() and is unfortunately unavoidable as far as I can tell, just ignore it and use the red part).
Example 2
If we slightly edit the code so the yield is after the line that errors like so
local CoroutineWithStack = require(4851605998)
local function Add(a, b)
return a + b
end
local function Func(a, b)
local Num = Add(a, b)
wait(2)
return Num
end
local function DoThing()
coroutine.wrap(Func)(1, "b")
end
DoThing()
With normal coroutine.wrap we get the following error:
As you can see, it has missed the line the error occured on (line 4) and the line where the Add(a, b) function ran (line 7)
If we replace the coroutine.wrap with CoroutineWithStack we get the following error
Modified code
local CoroutineWithStack = require(4851605998)
local function Add(a, b)
return a + b
end
local function Func(a, b)
local Num = Add(a, b)
wait(2)
return Num
end
local function DoThing()
CoroutineWithStack(Func, 1, "b")
end
DoThing()
And once again, you can see it now correctly has the entire stack
You may be able to tell I’m both very excited and very tired (it’s past midnight) right now so I apologise for any mistakes/cringey parts in the post but I’m very happy I managed to get this working (almost gave up) and wanted to share this. If I’ve missed an easier way to do this let me know.