My take on WaitForDescendant, thoughts?

I made a WaitForDescendant function for my game. What I’m not too happy about is that fact that it seems a little long and unclean. I don’t think my method of detecting a long yield is too great. What can I improve?

return function(parent: Instance, name: string)
	local TIMEOUT = 10
	
	local part do
		for _, v in ipairs(parent:GetDescendants()) do
			if v.Name == name then
				part = v
			end
		end
	end
	
	if not part then
		coroutine.wrap(function()
			task.wait(TIMEOUT)
			
			if not part then
				warn(("Descendant %s was not found in Instance %s, infinite yield possible"):format(name, parent.Name))
			end
		end)()
		
		repeat
			local addedPart = parent.DescendantAdded:Wait()
			part = if addedPart.Name == name then addedPart else nil
		until part
	end
	
	return part
end

There are very few use cases that you should be directly in need of a WaitForDescendant function. Anyway, here’s a version that’s probably more ideal.

local function WaitForDescendant(descendantOf, str)
	assert(typeof(descendantOf) == "Instance", "Invalid type for argument 1 (descendatOf)")
	assert(typeof(str) == "string", "Invalid type for argument 2 (str)")
	
	if descendantOf:FindFirstChild(str, true) then
		return descendantOf:FindFirstChild(str, true)
	else
		local t = {tick(), false}

		repeat
			if not t[2] and tick() - t[1] > 10 then 
				warn("Infinite yield possible on "..tostring(descendantOf)..":WaitForDescendant("..str..")")
				t[2] = true
			end
			
			descendantOf.DescendantAdded:Wait()
		until descendantOf:FindFirstChild(str, true)
		
		return descendantOf:FindFirstChild(str, true)
	end
end

You’re welcome to wrap the warning inside of a different thread if you’d like to. Either way, I would implore you to re-evaluate your code structure if you are in consistent need of WaitForDescendant.

Additionally, if your concern is its general efficiency then you could return to your checking strategy of DescendantAdded against string. Your code is probably better there.

6 Likes
smaller/better function
function WaitForDescendant(Object: Instance, DescendantName: string, TimeOut: any)
	TimeOut = if tonumber(TimeOut) then tonumber(TimeOut) else 5

	local OSClock = os.clock()
	
	while true do task.wait()
		local DescendantFound = Object:FindFirstChild(DescendantName, true)

		if os.clock() - OSClock >= TimeOut or DescendantFound then
			return DescendantFound or warn("Infinite yield possible on '"..Object.Name..[[:WaitForDescendant("]]..DescendantName..[[")']])
		end
	end

	return nil
end

print(WaitForDescendant(workspace.Baseplate, "find me!"))
larger function that used functions
function WaitForDescendant(Object: Instance, DescendantName: string, TimeOut: any)
	TimeOut = if tonumber(TimeOut) then tonumber(TimeOut) else 5
	
	local OSClock = os.clock()
	local AddedConnection = nil
	local NameConnections = {}
	local DescendantFound = nil
	
	local function DescendantAdded(Descendant)
		if DescendantName == Descendant.Name then
			DescendantFound = Descendant
		end
		
		table.insert(NameConnections, Descendant:GetPropertyChangedSignal("Name"):Connect(function()
			if DescendantName == Descendant.Name then
				DescendantFound = Descendant
			end
		end))
	end
	
	local function DisconnectFunctions()
		AddedConnection:Disconnect()

		for _, Connection in ipairs(NameConnections) do
			Connection:Disconnect()
		end
	end
	
	for _, Descendant in ipairs(Object:GetDescendants()) do
		DescendantAdded(Descendant)
	end
	
	AddedConnection = Object.DescendantAdded:Connect(DescendantAdded)
	
	while true do task.wait()
		if os.clock() - OSClock >= TimeOut or DescendantFound then
			DisconnectFunctions()
			
			return DescendantFound or warn("Infinite yield possible on '"..Object.Name..[[:WaitForDescendant("]]..DescendantName..[[")']])
		end
	end
	
	return nil
end

print(WaitForDescendant(workspace.Baseplate, "find me!"))

I know there is already a solution, but I decided to recreate the function as well
I also included the TimeOut parameter that the original WaitForChild has

I created two functions, the shorter one is more performant and the second one was made to see how it would work with DescendantAdded

this is just my take on WaitForDescendant

2 Likes

You’ll always be better off with DescendantAdded:Wait() over some constantly iterating loop. In the instance that you need a timeout, you can write that to a separate thread altogether (or any other method you see as reasonable).

The reality is that WaitForDescendant acting around a loop is actually less ideal because you may miss out on triggering your return asap because the wait() could be caught up in a yielding state during Resume Wait States portion of task scheduler- which means you’ll miss however many cycles that your yield takes up.

Also, please don’t use the second option. instance.DescendantAdded is already accessible through any instance. This longer option also holds pointless pollution of WaitForDescendant’s namespace.

I imagine WaitForDescendant as a top-level function, so there is no reason it shouldn’t be localized.

thanks for the reply
I already stated that the second function was just a test to see how the function would work in a different way, never said to use it

decided to update the function a bit
function WaitForDescendant(Object: Instance, DescendantName: string, TimeOut: any)
	TimeOut = if tonumber(TimeOut) then tonumber(TimeOut) else 5

	task.delay(TimeOut, function()
		warn("Infinite yield possible on '"..Object.Name..':WaitForDescendant("'..DescendantName..[[")']])
	end)

	local function FindDescendant() task.wait()
		local DescendantFound = Object:FindFirstChild(DescendantName, true)

		return if DescendantFound then DescendantFound else FindDescendant()
	end
		
	return FindDescendant()
end

print(WaitForDescendant(workspace.Baseplate, "find me!"))

anyways I made some changes to the code
first I am using recursion, second I found out that TimeOut isn’t supposed to stop the code from running(so I fixed that)

didn’t really feel like I had a need for DescendantAdded:Wait() imo
never localized the function either, if you want to localize it then you can do that yourself

just a tip for you
I saw that you were using tick() in your code, you should use the os library instead

You’re correct here.

It turns out that localization doesn’t matter because taking another look at OP’s use-case would insinuate this function is used for higher order within some greater structure.

This method of recursion still relies on task.wait(), where my point is that in this case task.wait() should be avoided to allow the code to proceed as soon as the next task scheduler cycle begins (it also saves you the trouble of nesting functions for no reason). Additionally, loops are almost always preferred to recursion to avoid a call-stack overflow. “The most-common cause of stack overflow is excessively deep or infinite recursion, in which a function calls itself so many times that the space needed to store the variables and information associated with each call is more than can fit on the stack.” source.

I can’t only use DescendantAdded:Wait() because the child can also be found if one of the descendants names are changed, that was a flaw in your code

we can fix this by using Descendant:GetPropertyChangedSignal(“Name”):Wait() and Object.DescendantAdded:Wait()

the question is, what is the most efficient way to implement these two events

also yea ur right about the recursion