Instance.Destroyed Event

I got you covered:

  • To free up memory properly, sometimes we might move things to nil, using AnchestryChanged isn’t ideal and we cannot access Instance.RobloxLock
  • Being able to accurately detect when an Instance is destroyed as we might need to cancel a block of code
5 Likes

And there’s more! I maintain several libraries used by different developers that need to tell the difference between a destroyed instance and a temporarily parented to nil instance. I am literally unable to create a library without warning developers not to parent instances to nil at this time, preventing some desirable use-cases.

I should also note that the method above is not always reliable. I have noticed memory leaks due to the fact that function detects an Instance as not being destroyed.

7 Likes

Was thinking about something. Would it be possible to use the fact that :Destroy() disconnects all connections to events as a way to detect this?

You would be forced to manually check whether signals have been disconnected, and it would introduce performance issues into the game.

It would, but it shouldn’t be much of an issue for a few instances.

For example, you can connect to AttributeChanged a function that increments a variable when a “DestroyCheck” attribute is changed. Then call :SetAttribute whenever the Parent is set to nil and compare the old value to the new one to check if the connection still exists.

There is some old code way up in this thread that I wrote that does this. Basically, you need to look for:

  • The object going from in-game to in-nil while losing connections (Destroyed)
  • The object going from in-game to in-nil, but not losing connections (not destroyed, but might get garbage collected or destoyed later)
  • Detecting garbage collection using some sort of weak instance reference (weak tables do not perform properly here). An ObjectValue in nil typically works for this, but you’ll need to check it often, and this is behavior that may change at any time.
  • Detecting when an object has already been destroyed. This is impossible without trying to set the parent.

You only have to do this for objects that are in nil, but not destroyed. Otherwise, you can just listen on AncestryChanged and check if the event connection is disconnected a frame later.


I personally do not suggest checking for Destroyed for typical game code. It is more reliable and safe to always keep your objects under game and assume that once in nil that they will never return.

There is the edge case that an object goes in nil, is not destroyed, but still gets garbage collected. This is a completely valid series of events, but hard to program for and part of what “Destroyed detection” entails. Not all Instances that are removed from memory have been :Destroy()ed!


Anyway, here’s some new code I’ve written that detects both Destroyed and garbage collection. I’ve kept this code simple because I want to to be easy to understand.

Notably, to respond to an instance’s garbage collection your callback cannot hold any references to the instance, otherwise it’ll prevent garbage collection. You can still hold a reference for detecting Destroy, but if the object is never :Destroy()ed after you attach your callback then you will cause a memory leak.

I only personally recommend using these techniques to debug memory leaks. For all other purposes, I suggest just using an AncestryChanged connection and treating not instance:IsDescendantOf(game) as Destroyed.

8 Likes

Doing some more thinking about this, I feel like there’s two parts to Instance lifetime clean-up:

  • The Roblox-side. This happens at garbage collection.
  • The Developer-side. This happens when you remove all Lua data related to an Instance.

Developer-side clean-up always has to happen first.

If you have any Developer-side clean-up, you have to claim control over the Instance’s lifetime. This means that you are claiming to clean up your Developer-side data before expecting the Roblox-side clean-up to happen. If you don’t claim control over the Instance’s lifetime, you will cause memory leaks!


In order to claim control over an Instance’s lifetime, you need an authority on the lifetime. This lifetime authority lets other parts of the codebase detect when an Instance’s lifetime is over and clean-up where appropriate.

The simplest lifetime authority is the Instance’s ancestry. This acts as a built-in, framework-agnostic authority. This is why I suggest using it. This is more-or-less a Roblox-controlled authority that is developer-triggered.

An alternative right now is to have a module acting as the lifetime authority that can trigger clean-up code.

A future alternative will be a semi-standard “LifetimeOver” attribute to act as the Instance’s lifetime authority. To clean-up the Instance, you change the attribute first to trigger Developer-side clean-up, then you Destroy the Instance to trigger Roblox-side clean-up.



What this feature request is suggesting is that the Instance’s Destroy state become a new framework-agnostic lifetime authority. This is not wrong, but it is fundamentally misleading and can lead to hard-to-diagnose memory leaks.

What is misleading here is that using the Destroy state as the lifetime authority implies that Roblox has control over the Instance’s lifetime. It implies that when Roblox-side clean-up happens, Developer-side clean-up will happen too, which implies that Developer-side clean-up will happen even when :Destroy() isn’t called.

These implications are entirely false. Developer-side clean-up must happen first, so the developer must claim control over the Instance’s lifetime. If the Destroy state is the lifetime authority, then the developer must call :Destroy(), but this is implied nowhere and can catch many developers with memory-leak bugs.


Contrast the two approaches a third-party module can take:

  • “I will try to clean myself up when Destroy is called on the Instance. This means you must call Destroy on the Instance or a memory leak will happen, but I will not expose this requirement in my API at all!”
  • “I will only clean up when you call the clean up function. You need to clean up when you’re done with this Instance, and I am exposing this requirement in my API!”

The second option more clearly demonstrates a requirement for the developer to claim control over the Instance’s lifetime. This plays into ideas about API design and how explicitness creates safe, easy-to-use code.

The only reason that detecting Parent = nil is particularly safe is because it’s common-sense that an object cannot be cleaned-up while it’s somewhere in the game, and because Parent = nil is a prerequisite for Roblox-side clean-up. Parent = nil effectively gives you Roblox-authoritative Instance lifetimes with no risk of memory leaks.

3 Likes

It is currently impossible to detect when an Instance is destroyed with accuracy. My main use for this is with making modules for use by other people, because I can’t and shouldn’t have to control what they do with Instances. If I give an Instance extra behavior without wrapping it somehow, I should be able to stop that behavior when the Instance is destroyed. It shouldn’t have to come with the caveat “don’t set this thing’s Parent to nil or it will stop working”.

And to be clear, when I say “impossible” I mean literally impossible. Because Destroyed doesn’t disconnect events until a frame later, you can’t check directly using AncestryChanged or any of its cousins without yielding at least a frame, which can cause a few problems, including false positives.

To be blunt, I don’t really care if Roblox says we shouldn’t parent stuff to nil. People do, and we have to deal with it. It seems ridiculous to make us jump through hoops for something as trivial as knowing when we can’t use an Instance anymore.

20 Likes

Add me on to the list of people who’s mental health would benefit greatly from this addition

44 Likes

Today I found out then when a model is destroyed by the server, the descendants of that model will not have their parents set to nil on the client. This means using Instance.Parent == nil is not a reliable way to check if an instance has been destroyed, necessitating the use of :IsDescedantOf(game), which then makes its own problems as it means you cannot parent one instance to another before the parent instance itself has been made a descendant of game, or else it will trip the destroy code.

25 Likes

I think we can also benefit from a .IsDestroyed property or function for cases when we need to check if an Instance has been destroyed.

4 Likes

Dropping in here to mention this code no longer works. The omnipotent gods that govern garbage collection don’t seem to enjoy ObjectValues, as far as I can tell.

As part of my work on my Fusion library, I’ve come up with a new solution that should work, even under deferred events (which the old solution notably wouldn’t support because of the bindable event-based fast spawn implementation).

Here’s the code as it currently stands - it’s extracted directly from Fusion’s source, so you’ll need to modify it a bit to make it work:

cleanupOnDestroy() as implemented in Fusion (github.com)

In the future, when I publish Fusion to a GitHub repo, you’ll likely be able to find a unit-tested and actively maintained version there (unless it gets removed in favour of something else, in which case I’ll come back here and update this post) :slightly_smiling_face:

9 Likes

Also dropped in to mention Roblox are making quite a significant change to events that will break AncestryChanged.

For all intensive purposes, it’s a hacky way of detecting when an instance is destroyed, because someone could parent an Instance to nil and fire the event even though it wasn’t truly destroyed.

1 Like

Are you referring to deferred events? If so, then that doesn’t corroborate with my findings developing the method above.

Deferred events shouldn’t prevent AncestryChanged from firing for instances being destroyed or moved out of the data model - it simply changes the time at which that event is run within the frame, which isn’t usually important for cleanup code.

If you consider moving an instance out of the data model to be effectively equal to destruction in all cases in your codebase, and only parent things to nil when you’re done with them, then it’s not a particularly egregious way of detecting destruction imo - though in any case I prefer to keep track of things and clean them up manually if I can.

3 Likes

I would love to see this added in Roblox, I’d just encountered a situation where this would help really well. It would also allow you to run traditional RBXScriptSignal functions like :Wait() which would not work with AncestryChanged.

It has been added already! It is Instance.Destroying. It runs before the instance is destroyed. Do note that you need to use a script outside of the instance being destroyed to make it work.

2 Likes

Really? What do you mean by external script?

Never mind that, I looked at the documentation and found out how to implement it.