EDIT - some small spelling corrections.
Hi! I wouldn’t say this is a bug, but rather expected behaviour. I can not confirm how require() works internally, however, I can provide some observations and try to prove them. See the steps taken below.
My hypothesis:
-
Once the module script is required, it is strongly referenced, so there is no way for it to be clear from memory by lua garbage collector, unless the script that holds the reference is destroyed as well. Module is available to the script at all times.
-
When the module is required, itscontent is loaded into the script and is thus part of that script, so strong reference applies.
Attempt 1
Module script code
game:GetService('Debris'):AddItem(script, 3)
while (true) do
game:GetService('RunService').Heartbeat:Wait()
print("Hi")
end
return nil
Script code
require(script.Module)
Hierarchy:
Findings: Module code continues to run after the module is destroyed.
Important detail is that Module is removed from the game, although it remains in memory. Proof:
game:GetService('Debris'):AddItem(script, 3)
while (true) do
game:GetService("RunService").Heartbeat:Wait()
print(script.Parent)
end
return nil
-- OUTPUT BEFORE REMOVING:
-->> Script
-- OUTPUT AFTER REMOVING:
-->> nil
Attempt 2
Same module script.
Script code
require(script.Module)
print("EXECUTED") -- never runs, same hierarchy.
Attempt 3
Wrapper code
wait(5)
script.Script:Destroy()
Script code
require(script.Module)
print("EXECUTED") -- doesn't run.
Same module script.
Hierarchy:
Findings: Module script doesn’t run anymore, neither does script (parent). No values printed in the output. print(“EXECUTED”) doesn’t run either.
Attempt 4
Module script code
game:GetService('Debris'):AddItem(script, 3)
local module = {}
function module.sayHello()
while (true) do
game:GetService("RunService").Heartbeat:Wait()
print(script.Parent)
end
end
return module
Script code
local Module = require(script.Module)
wait(5) -- until destroyed
Module.sayHello()
Code runs successfuly!
Conclusion
As previously said, I don’t know how require works exactly, but can confirm that at least one of above stated hypothesis is true and valid. There is perhaps the difference between
require(script.Module)
and
local Module = require(script.Module)
But ultimately, it isn’t important. Former is a standard way of requiring a module, and direct call is used when the script has to be ran immediately. If we try to destroy the module script before requiring it, that also doesn’t make sense, because module is not accessible, probably scheduled for removal from memory, and no game paths are valid.
This doesn’t seem like a bug. No workaround is needed, because it all comes down to how code is written. Infinite loops in module scripts behave normally, but are not the best practice. A better way would be wrapping them in functions, which are then called by other scripts in various ways. To my knowledge, module scripts that remain idle in memory are pretty performance friendly, unless they are running and doing heavy tasks.