Recursive WaitForChild, with timeout (no loops used, purely coroutine and event based!)

Hello!
I’ve recently been cleaning up my scripts, and found out that I use loops a lot.
ROBLOX doesn’t offer a recursive parameter for WaitForChild, so I usually have been doing ugly stuff like

repeat wait() until Obj:FindFirstChild(Name, true) or (Delay and tick() - Start > Delay)
return Obj:FindFirstChild(Name, true)

this is obviously very inefficient, not to mention that it uses loops instead of yielding the thread.
So, I’ve created this simple function to do this for you!

local function WaitForDescendant(Parent: Instance, Child: string, TimeOut: number?)
    local AlreadyFound = Parent:FindFirstChild(Child, true)
    if AlreadyFound then
        return AlreadyFound
    end

    local Connection
    local TimeOutThread

    local Thread = coroutine.running()

    Connection = Parent.DescendantAdded:Connect(function(Descendant)
        if Descendant.Name == Child then
            Connection:Disconnect()
            if TimeOutThread and coroutine.status(TimeOutThread) == "suspended" then
                task.cancel(TimeOutThread)
            end

            task.spawn(Thread, Descendant)
        end
    end)

    if TimeOut then
        TimeOutThread = task.delay(TimeOut, function()
            if Connection.Connected then
                Connection:Disconnect()
                task.spawn(Thread)
            end
        end)
    end

    return coroutine.yield(Thread)
end
19 Likes

Just a few nit-picks, coroutines and delays aren’t really ideal and have a plethora of problems themselves.

delay can sometimes just… not work, coroutines eat stack traces, amongst other issues. While it’s not very efficient, a loop is your best bet.

Never had issues with either of the problems you stated.
For stack traces, I’m unsure how the function could even error.
Either way, it yields the current thread; it doesn’t create a new one so the error traces should remain intact, no?

3 Likes

Doesn’t waitforchild yield forever until the subject is found, so why do you need a recursive method?

Recursive means it scans the descendants too. Like if there is a part deep inside a model, you’d have to call on it’s parent instance directly, and not an instance above it.

Anyways this is old code and shouldn’t be used.

As @VortexColor mentioned, this is the equivalent of :FindFirstChild(Name, true) for WaitForChild. I also agree that it’s old code, and I’ve revised the function since to use typechecking along with the new task API, which also supports a (proper) timeout:

local function WaitForDescendant(Parent: Instance, Child: string, TimeOut: number?)
    local AlreadyFound = Parent:FindFirstChild(Child, true)
    if AlreadyFound then
        return AlreadyFound
    end

    local Connection
    local TimeOutThread

    local Thread = coroutine.running()

    Connection = Parent.DescendantAdded:Connect(function(Descendant)
        if Descendant.Name == Child then
            Connection:Disconnect()
            if TimeOutThread and coroutine.status(TimeOutThread) == "suspended" then
                task.cancel(TimeOutThread)
            end

            task.spawn(Thread, Descendant)
        end
    end)

    if TimeOut then
        TimeOutThread = task.delay(TimeOut, function()
            if Connection.Connected then
                Connection:Disconnect()
                task.spawn(Thread)
            end
        end)
    end

    return coroutine.yield(Thread)
end

Decided to make an updated version of this, let me know what I can change.

local function WaitForDescendant = function(Parent: Instance, Child: string, TimeOut: number, Instance_Type) --//INSTANCE_TYPE can be left blank but is used for type checking
	local Already_Initialized = Parent:FindFirstChild(Child, true)
	if Already_Initialized then return Already_Initialized end --//Descendant was already initialized
	--//
	if not TimeOut then --//Timeout is used as a safety measure similar to :WaitForChild(Instance, (time)) where time = TimeOut
		TimeOut = 60
	end
	--//
	local InstanceAddedCheck = nil
	local TimeOutThread = nil
	local Thread = coroutine.running()
	--//
	InstanceAddedCheck = Parent.DescendantAdded:Connect(function(Descendant) --//Check for descendants being initialized/added to Parent
		if Descendant.Name == Child then else return end
		if Instance_Type ~= nil then
			if Descendant:IsA(Instance_Type) then else return end 
		end
		if InstanceAddedCheck ~= nil then 
			InstanceAddedCheck:Disconnect()
			InstanceAddedCheck = nil
		end
		if TimeOutThread and coroutine.status(TimeOutThread) == "suspended" then
			task.cancel(TimeOutThread)
		end
		task.spawn(Thread, Descendant)
	end)
	
	TimeOutThread = task.delay(TimeOut, function()
		if InstanceAddedCheck ~= nil then else return end--or typeof(InstanceAddedCheck) == "RBXScriptConnection" and InstanceAddedCheck.Connected then else return end
		InstanceAddedCheck:Disconnect() --//Disconnect + set to nil
		InstanceAddedCheck = nil
		task.spawn(Thread) --//Resume coroutine? But return nil as TimeOut has been reached
	end)
	
	return coroutine.yield(Thread) --//Returns result of coroutine (Descendant)
end

Thanks! Will definitely be using this! I don’t see any issues with the code, you should turn this into a community resource.

Bit confused, what was changed in terms of behavior?

The script performs the same action, but to my knowledge simply using Disconnect() doesnt remove the connection from memory and setting to nil prevents memory leaks. The general consensus here is to set RBXScriptConnections to nil as the console still points to the variable being used even if the function is disconnected.

Once the function stops executing it automatically gets garbage collected, and thus goes out of memory. its the same reason as why you dont have to manually set all variables to nil after e.g. a for loop.

I just wanted to share since this changed my life.

@sleitnick has a powerful utility class for this kind of thing and it incorporates promises for better error handling and integration it’s called WaitFor. Check out his github page and all of the utilities he uses for Roblox development, and for more information about promises and waitfor.

Some of the things this module can do:

  1. WaitFor.Child() – same as WaitForChild()
  2. WaitFor.Children({child1, child2}) – can wait for multiple children in 1 line
  3. WaitFor.Descendant() – This is the one that has “recursive-like” but is not really recursive.
  4. WaitFor.Descendants() – same as number 3 but multiple descendants