Non-yielding unperformant code in metamethod causes problems in ModuleScript

Export module is requiring APIService, which is then requiring a 6,050,000characters-long module script through GetDependencies, a script that returns metatable using the __index metamethod to change it’s functionality to requiring a script of the indexed name descendant of Root (in this case, the plugin’s main script). This leads to the following exceptions:

attempt to yield across metamethod/C-call boundary
ModuleScript cannot resume non-suspended awaiting coroutine

In certain conditions I don’t specifically know, the exception thrown was changed to:

cannot resume dead coroutine

which was misleading and caused me to delay the solving of the problem an hour or so.

Export is subsequently requested by the Plugin’s main script, ScriptSynchronizer, and that’s stated as the most recent error traceback call line. Instead, if the metamethod plainly returns the instance of the ModuleScript, even if that same ModuleScript is required afterwards outside the metamethod, no problem arises. Note that both ScriptSynchronizer has to manually require Export, and Export has to manually require APIService in order for the error to be solved; APIService doesn’t use GetDependencies and therefore never caused any issue.

Expected behavior

This might be expected behaviour, but it’d be nice if the errors were more descriptive or specific, if the documentation explained the timeout time of required scripts within metamethods (which I’m assuming is one heartbeat), and then even if this timeout was customizable.

1 Like

The error is for when you yield, not for when you hit some kind of timeout. Yielding functions require special code by the engine/Luau in order to operate correctly, you are unable to call them in metamethods due to “significant performance implications”.

Yielding functions (such as task.wait or :WaitForChild) usually have the Can Yield tag on the Documentation:

image

I believe a workaround is to put the require inside of a coroutine.wrap/task.spawn or similar, it should allow your modules to yield when being require’d.

Something that Roblox could look into is improving the error messages, such as the attempt to yield across one to point to the function itself which is inside the Module, instead of just the require which eventually calls it.

1 Like

Okay, you have a point, but then again how is my code yielding? I specifically ensured that there are not any task.wait()s or :WaitForChild()s by commenting them, yet the code was still throwing that same exception. What I naturally assumed was that the operation of requiring a long module was taking too long and was therefore determined as ‘yielding’.

“Yielding” specifically means giving control back to the Engine. If you never yield in a while true do loop, the Engine eventually, forcefully, takes control by throwing the “Script exhausted allowed execution time” error. Otherwise, if there was no timeout, Scripts would be able to run their own code forever and freeze the game window, as the Engine has no chance to render anything and to tell the Operating System “Hey, I’m alive!”, causing it to go into a “Not Responding” state, and get that white overlay on Windows or the beachball on Mac. This can happen temporarily in an infinite non-yielding loop as well, however the timeout ensures the Engine will eventually un-freeze and become responsive again.

Therefore, in order to “yield”, you must explicitly call a function that has the capability to yield, allowing the Engine to temporarily pause the Script and do other important work.

From what you’ve shown, it seems that either the Export module or the APIService module which it internally require’s has a yielding call in it. Obviously, this is all subject to my own knowledge. Perhaps they recently made require yield in certain circumstances, but that doesn’t seem likely to me.

Also, try running the require within a pcall. pcalls support yielding, so maybe it’ll work

Thx for the feedback. Solving this would lead to performance issues in the platform unfortunately.

Okay, but what is the correct interpretation though? It’d be nice to know why this is happening in order to be able to avoid it more efficiently in the future; is @Judgy_Oreo’s guess correct?