How to make "clean up" scripts work with deferred engine events?

Hello, does anyone know how to get “clean up” scripts to work with deferred engine events?

Script.Destroying never fires.

script.Destroying:Connect(function()
    -- Cleanup stuff
end)
2 Likes

This does work but I’m not sure how it is on performance.

task.spawn(function() 
   script.Destroying:Wait()
 -- Cleanup
end)

Eh, that would work. but I’m going to see if anyone else knows a better way to fix this.

Is this even supported? It needs to run immediately to work, which is what deferred event handling is supposed to completely stamp out from the engine.
I just tried script.ChildAdded:Wait():Destroy() and it worked in deferred mode and didn’t in immediate mode, so maybe it will work forever.

As for the cleanup script, the reason why Destroying fires is because, whenever a script is disabled (including by moving it to a place where it can’t run, such as nil), every connection associated with it is disconnected. In Deferred mode, this tends to happen before the events have a chance to fire.

What you should do is reassign the “blame” for the creation of this Destroying event to another script.

Set your cleanup function and instance somewhere and use a connection (created by a script that will never be destroyed) to notify another script to make the connection.

Something like

local inst, fn, conn
local bindable = Instance.new("BindableEvent")
-- This connection blamed on this script, as are any other connections made by this event handler
bindable.Event:Connect(function()
conn = inst.Destroying:Connect(fn)
end)
-- This function is run by other scripts, so it MUST NOT connect any events, otherwise the engine will blame them on the caller
return function(inst2, fn2)
inst, fn, conn = inst2, fn2, nil
bindable:Fire()
return conn
end

If your object is moved to nil or generally made useless, but never destroyed, then Destroying will never fire. [PSA: Connections can memory leak Instances!](It might also never get garbage collected.)
At least the Destroying:Wait() will get disconnected when the script gets disabled (though it won’t run the cleanup code, which defeats the point)

I have never tested this code or actually used anything to this effect, but I know it will work and plan to use this trick.

Another option is to use DescendantRemoving rather than Destroying.
Get a ModuleScript (or a normal script that exposes its interface through shared or something) that connects to game.DescendantRemoving, looks for removed instances in a table: {[Instance]: ()->()}, calls it and removes it from the table. That will “clean up” anything going to nil that is registered here. It will run really often on unrelated stuff (because it’s game.DescendantRemoving :grimacing:) and clean up anything that goes to nil even when it was put there intentionally, with intent to put it back in workspace.

1 Like