Roblox Coroutine wait() and resume() bug

When a coroutine is waiting it can be resumed immediately and later causes an error when roblox attempts to resume the coroutine after waiting.

This error is easily reproducible in any script.

local co = coroutine.create(function() print("Hello, world!") wait(3) print("Goodbye.") end)
coroutine.resume(co) -- Starts the function, prints "Hello, world!", then waits.
coroutine.resume(co) -- Immediately resumes the coroutine and prints "Goodbye." after wait call.
-- Afterward roblox attempts to resume the coroutine and fails.

Not a bug, that’s how wait and the Roblox thread scheduler work. CC @woot3

I’m aware that its how the scheduler works, however it is still a bug, the scheduler should not be throwing errors during normal operation and a yielding function that hasn’t returned should not allow the coroutine to resume.

You’re explicitly resuming a thread which is being handled by the internal scheduler. The scheduler does not expect you to do this so it seems reasonable that it should throw an error.

Imagine having written some code which accidentally resumes a thread twice. Were this error not there it could be a nightmare to debug and realize the mistake that you’ve made.

If you want this behavior, you can implement it yourself.

function wait(duration)
    local thread = coroutine.running()
    delay(duration, function (...)
        if coroutine.status(thread) == "suspended" then
            coroutine.resume(thread, ...)
        end
    end)
    return coroutine.yield()
end
3 Likes

Correct, however, the wait has not been fulfilled it should not return until it is finished waiting. This is not an issue specific to wait(); any yielding function should fulfill or error before returning.

As an example, a yielding function that reads a value from disk when explicitly resumed should not immediately return and then later error because it failed to read before being resumed. Likewise wait should also fulfill its promised task. When a read function that was yielding doesn’t have its value to return yet it should either yield again or throw an error.

You can also implement this behavior yourself by:

Yielding until the wait time has been achieved…

function wait(duration)
	local thread = coroutine.running()
	local fulfilled = false
	local yield
	delay(duration, function (...)
		fulfilled = true
		if coroutine.status(thread) == "suspended" then
			coroutine.resume(thread, ...)
		end
	end)
	while not fulfilled do
		yield = {coroutine.yield()}
	end
	return unpack(yield)
end

or throwing an error/warning immediately upon resume rather than later…

function wait(duration)
	local thread = coroutine.running()
	local fulfilled = false
	local yield
	delay(duration, function (...)
		fulfilled = true
		if coroutine.status(thread) == "suspended" then
			coroutine.resume(thread, ...)
		end
	end)
	yield = {coroutine.yield()}
	if not fulfilled then
		warn("coroutine explicitly resumed before waiting.")
	end
	return unpack(yield)
end

The issue is not one of whether this can be done, its that this should not be the default interaction. Its error-prone and unexpected. When it throws an error the error is unhelpful. It means that wait() does not fulfill its promise of resuming after an elapsed time and only errors later rather than doing so immediately.

That is why I call this a bug. Failing to fulfill a task and failing to throw a proper error or warning is an issue that needs to be fixed and can be fixed quite easily.

This post has been edited. The post previously included code that would not capture all return values of yield. This has since been corrected.

3 Likes