Script does not resume, after the required ModuleScript returns

here’s some example code.

-- script
require(some_module);
print("c");
-- some_module
print("a");
local c = coroutine.running();
task.delay(2, function()
   coroutine.resume(c);
end)
coroutine.yield();
print("b");

return nil;

you would expect the output to be:
a
b
c

the actual output is:
a
b

c never prints, in fact if you were to check the script’s coroutine’s state you will get result of ‘suspended’.

you can check the script’s state like this.

-- script
_G.cor = coroutine.running();
local module = require(some_module);
print(module); -- always nil
print("c");
-- some_module
print("a");
local c = coroutine.running();
task.delay(2, function()
   coroutine.resume(c);
end)
coroutine.yield();
print("b");

print(coroutine.status(_G.cor)); -- 'suspended'

-- if you do resume the script's coroutine, 
-- the value of require(some_module) will be nil, 
-- regardless of what the module actually returns.
task.defer(_G.cor);

local module = ...;

return module;

note that this side effect doesn’t occur if the module yields through a task library function or roblox function (for example WaitForChild()).

Expected behavior

this is what docs says should happen, on the ModuleScript api reference page.

“Note that the first call to require() will not yield (halt) unless the ModuleScript yields (calls task.wait() for example), in which case the current thread that called require() will yield until the ModuleScript returns a value.”

the module does return a value, but the script doesn’t resume execution.

I believe the issue is caused by manually yielding and resuming the same coroutine that require() is executing on

require() appears to have its own continuation handling when the ModuleScript yields. When you do:

local c = coroutine.running()

task.delay(2, function()
    coroutine.resume(c)
end)

coroutine.yield()

you are manually resuming the coroutine before Roblox’s require() continuation gets a chance to resume it normally

That’s also why print("b") executes, but print("c") never does. The ModuleScript itself continues correctly after the coroutine.resume(), but the caller’s require() continuation is left in a suspended state

You can see the difference by replacing the manual coroutine yield with a scheduler yield:

-- some_module
print("a")

task.wait(2)

print("b")

return nil

Then the output is:

a
b
c

The same seems to apply to WaitForChild() and other Roblox/task scheduler yielding functions, which explains why the side effect doesn’t occur with those

So I don’t think the problem is that the ModuleScript failed to return. It does return. The problem seems to be that manually resuming the coroutine bypasses/breaks the continuation that require() uses to resume the caller

If this is reproducible consistently, I’d consider it an engine bug/edge case in the interaction between require() and manually resumed coroutines rather than a normal Luau coroutine behavior

i did mention that.

and i didn’t say the problem, was that the module failed to return.

we couldn’t really know what causes this or how roblox manages the resumption of the require() caller’s execution, because that code is closed source.

In case, where you absolutely need to do coroutine.yield() during the require of a module, you would implement that inside a function instead of a module, to have it work normally, however that would be a workaround if the programmer, thought that putting that code inside a module script was more appropriate.

I consider this a bug, simply because it doesn’t follow the expected behavior as explained in the docs.

as for reproducing the bug consistently, for me every time i run the code, from the first example, i get the same result, is that different for you?