BindableEvent:Wait() never yields after destroy?

Hello, I came across this and I need some insight on what’s going on.

print("Hello world!")

local bind = script.Event; --BindableEvent

task.delay(1, function()
    print("destroy");
    bind:Destroy();
end)

task.delay(2, function()
    print("fire");
    bind:Fire();
end)

print("test");
bind.Event:Wait();
print("yielded")

yielded never prints, does that mean it’s indefinitely yielding or is the thread just terminated completely?

Thanks in advance
Khronos~

Yes, it’s infinitely yielding. Here’s a smaller example where it never prints “finish”:

local bind = Instance.new("BindableEvent")
print("start")
bind:Destroy()
bind.Event:Wait()
print("finish")

It’s indefinitely yielding, which would create a memory leak for most applications.

Proof:

local bind = Instance.new("BindableEvent")

local weak = setmetatable({
	thread = task.spawn(function()
		bind:Destroy()
		bind.Event:Wait()
	end)
}, {__mode="kv"}) -- weak table so that we're not affecting the test by keeping a reference around

while task.wait(1) do
	print(coroutine.status(weak.thread))
end

That will print suspended forever.

I thought suppositely :Destroy()-ing a instance should disconnect it from every event/yields. Is this a bug?

I’m sorry, I was completely wrong. Obviously my thread is waiting forever—I never Fire()d my event!

Give me a minute to come up with a better answer :slight_smile:

1 Like

Okay, new answer. I still pretty much stand by what I said. In your case, the thread will be suspended as if you never called Fire.

Destroying sets a parent to nil and disconnects events. It doesn’t do anything else.

A “Wait” is a connected event, so it’ll get disconnected, but nothing else will happen. The thread that was suspended will continue to be suspended.

Wait is probably implemented something like

local function MyWait(signal)
	local threadId = coroutine.running()
	local connection
	connection = signal:Connect(function(...)
		connection:Disconnect()
		coroutine.resume(threadId, ...)
	end)
	return coroutine.yield()
end

You can see how if connection got Disconnected before it was triggered—by Destroying the BindableEvent—the thread is never resumed, but that’s it.

That being said, there’s nothing stopping you from calling Wait and Fire on a Destroy'd object. Only currently-waiting threads are affected.

Here’s a full example, demonstrating:

  • Wait() and Fire() with no destroying (works fine, thread is dead)
  • Wait() and Fire() with a destroy in between (interrupted, thread is suspended forever)
  • Wait() and Fire() after destroying (works fine, thread is dead)
local bind = Instance.new("BindableEvent")

-- fire every 100 ms
task.spawn(function()
	for i = 1, 10 do
		task.wait(0.10)
		print("  fire", i)
		bind:Fire(i)
	end
end)

-- wait for first fire
local waitThread = task.spawn(function()
	-- should print 1
	print("waiting...")
	print("waiting: ", bind.Event:Wait())
end)

-- won't ever print...
local interruptedThread = task.delay(0.23, function()
	print("waiting but will be interrupted...")
	print("waiting but will be interrupted: ", bind.Event:Wait())
end)

-- ...because we destroy after the second fire
task.delay(0.25, function()
	print("destroy");
	bind:Destroy();
end)

-- prints
local afterDestroyThread = task.delay(0.35, function()
	print("waiting after destroy...")
	print("waiting after destroy: ", bind.Event:Wait())
end)

task.wait(10)

print("waiting thread:", coroutine.status(waitThread))
print("waiting but will be destroyed thread:", coroutine.status(interruptedThread))
print("waiting after destroy thread:", coroutine.status(afterDestroyThread))
2 Likes