Detecting infinite loops or permanent yields in .Start functions without blocking Loader

Hi everyone,

I’m building a ModuleLoader in Roblox that goes through all my Handlers (ModuleScripts) and calls their .Start() function if it exists. My loader works fine for normal handlers, but I have a problem:

If a .Start() function contains an infinite loop or yields forever, the loader gets stuck and can’t continue loading the remaining services. Using pcall only catches errors, but it doesn’t help with blocking functions.


Here’s my loader (very simple):

local Loader = {}

function Loader.Load(Services : {ModuleScript})
	
	for _, Service in Services do
		
		local Success, Result = pcall(require, Service)
		if Success then
			print("[MODULE-LOADER]: Service: '"..Service.Name.."' Loaded.")
		else
			warn("[MODULE-LOADER]: Service: '"..Service.Name.."' Failed to load. Error:", Result)
			continue
		end
		
		if Result.Start then
			local Success, ErrorMessage = pcall(Result.Start) -- If Ts Yields, the Loader Yields too
			if Success then
				print("[MODULE-LOADER]: Service: '"..Service.Name.."' Started.")
			else
				warn("[MODULE-LOADER]: Service: '"..Service.Name.."' Failed to Start. Error:", Result)
			end
			
		else
			print("[MODULE-LOADER]: Service: '"..Service.Name.."' has no Start function.")
		end
		
	end
	
end

-- No get function. Services get other modules with require for autocomplete, not that deep gng

return Loader



My only Solution was This:

local Success, ErrorMessage = pcall(function()
	task.spawn(Result.Start)
end)

But I don’t think this will catch errors inside Result.Start, because pcall only wraps the call to task.spawn. Since task.spawn runs Result.Start in a new thread, pcall returns immediately and cannot detect any errors that happen inside Result.Start itself.



Goal

If a .Start() function yields, the Loader will print that a specific functions yields,
and it will just continue looping trough the Handlers.

you can start delayed task with “max execution timeout” delay. If it triggers than the module yields and it’s timed out.

Spawning new threads does sound like the best option here, my suggestion would be to have a separate function which handles the loading and waits for responses. However, this can get very clunky very quickly, as maybe the best-case scenario from handling it in your loader would be to have two threads per start function, one for the start function itself and one to watch it to see if it yields or runs over a threshold.

I can see many issues with the system already, like horrible spaghetti code with extremely complicated logic and many race conditions; it’d be very difficult and annoying to do, and much less effective than other solutions.

My other suggestion is to think about your service design. Do you really need to have the loader run the start function in a protected call? Do you need the loader itself to handle all of this? Why can’t the service just handle the parts that need to be pcall’d, the parts that need to be run in separate threads, and have it return a straight Boolean there and then? It’d be better design and would run better as well. If you have many race conditions, or unneccessary yields, or parts that could error, it’d be much better to fix the code itself rather than try to patch over it.