PSA: You can get errors and stack traces from coroutines

Consider Quenty’s fastSpawn implementation:

function fastSpawn(func, ...)
	assert(type(func) == "function")

	local args = {...}
	local count = select("#", ...)

	local bindable = Instance.new("BindableEvent")
	bindable.Event:Connect(function()
		func(unpack(args, 1, count))
	end)

	bindable:Fire()
	bindable:Destroy()
end

An event, specifically a BindableEvent, is used to spawn the function in a new thread. Events have no delay (hence “fast”) and emit errors on the first resume of the thread.

Compare this to an almost-matching implementation that uses coroutines:

function fastSpawn(func, ...)
	assert(type(func) == "function")

	local thread = coroutine.create(func)
	local ok, err = coroutine.resume(thread, ...)
	if not ok then
		-- Insert preferred error-emitting mechanism.
		print("ERROR", err)
		print(debug.traceback(thread))
	end
end

This bit of error handling after the resume is necessary only once. If the thread yields from a Roblox function, then it will be handled by the Roblox scheduler from that point on, and any errors will be emitted as usual. If coroutine.yield is used, then the thread wont be handled any further, which matches the behavior of events.

The only drawback is that an error in the first resume of the thread cannot be emitted to output as an error. We have to make do with just printing it instead. However, let it be known that this information is not lost or inaccessible.

27 Likes

Is this not common knowledge?

It should get documented, not sure why it isn’t.

In regards to your fast spawn implementation, when you create the thread, the function can’t be a C function in 5.1, so it may be useful to just create a function to simply call the function.

local fastSpawn do
	local function call(f,...)
		-- avoid issue of creating a coroutine with a c function
		return f(...)
	end
	local function finish(thread,success,...)
		if not success then
			-- output error object + traceback
			print(debug.traceback(thread,"ERROR: "..tostring((...))))
		end
		-- on success return thread + true + results
		-- on fail thread + false + error
		return thread,success,...
	end
	function fastSpawn(f,...)
		-- verify f
		if type(f) ~= "function" then return error("function expected",2) end
		-- thread
		local thread = coroutine.create(call)
		-- results
		return finish(thread,coroutine.resume(thread,f,...))
	end
end
3 Likes

@Anaminus Thanks for the insight!

I made minor modifications and turned it into a Model & File for ease of access;

Code

feel free to edit it to your liking

→ Script

local coroutine = require(script.Coroutine)

local function Success(...)
	print(...)
end

local function Fail(...)
	error(...)
end

--coroutine.resume(coroutine.create(Success),"Success")
--coroutine.resume(coroutine.create(Fail),"Fail")

coroutine.spawn(Success,"Success")
coroutine.spawn(Fail,"Fail")

→ Module

local Coroutine = {
	create = coroutine.create,
	isyieldable = coroutine.isyieldable,
--	resume = nil,
	running = coroutine.running,
	status = coroutine.status,
	wrap = coroutine.wrap,
	yield = coroutine.yield,
	--
--	spawn = nil,
}

local NO_ERROR = false

function Coroutine.resume(thread,...)
	local Success, Error = coroutine.resume(thread,...)
	assert(NO_ERROR or Success, Error)
--	assert(Success, debug.traceback(thread,Error)) -- Honestly not sure what the differences are	
	return Success, Error	
end

function Coroutine.spawn(Function,...)
	assert(NO_ERROR or typeof(Function) == "function","function expected NOT: "..typeof(Function))
	local thread = coroutine.create(Function)
	local Success, Error = Coroutine.resume(thread,...)
	return Success, Error
end

return Coroutine

Coroutine.rbxm (1.4 KB)

2 Likes

PSA: Fast spawn does not use coroutines, so you are not getting stack traces from a coroutine.

You are getting stack traces from a spawn though. Quite nice as opposed to coroutine-based spawn :stuck_out_tongue: