FindFirstChild and WaitForChild: Addressing common bad practices and habits of each

well if my code stopped working and one of the reasons was that findfirstchild returned false then using

if workspace.Part then

end

and had an error, the output would tell me why the if statement didn’t pass, I mean that seems superior since it actually tells you what screwed up, but that’s just my opinion

1 Like

The method FindFirstChild. Is a method of the class Instance which the class Workspace inherits.
It returns nil if an object is not found. WaitForChild actually calls FindFirstChild if the object already exist. If not then it yields.

workspace.Part wouldn’t work and only errors because you tried accessing a property of workspace that doesn’t exist. In this case you don’t know if Part is a child of workspace or a property of workspace. Therefore it’s better to do workspace:FindFirstChild("Part") or workspace:FindFirstChildWhichIsA("Part").

The reason why you use a colon and not a period is because you are implicitly passing “workspace” as the first parameter. Otherwise it would look like workspace.FindFirstChild(workspace, "Part") the colon just makes it shorter. workspace:FindFirstChild("Part")

1 Like

Sorry if this is a stupid question, but here’s something that confuses me:

It seems really annoying to me to set up and existence check for every single time I use FindFirstChild(). Is this necessary everytime? Or should I just use the dot operator if I’m pretty sure it exists (for example, a modulescript that’s a child of a script or a part that is already in the workspace)?

Ideally dot syntax should’ve never been allowed for child access because of things like property confliction but since that ship’s long sailed it’s an acceptable method of indexing now. If the item you want to access is guaranteed existence, FindFirstChild is not required. Ideally you’ll want that for dynamic instances where existence is not guaranteed.

This thread is fairly old and there are some parts that overstep my knowledge on the topic as seen through conversation in the thread and I’ve never gotten around to rewriting the thread to specifically address bad practices and how they can be remedied. Standards change and I’m always looking to learn more as I discuss with other developers and play with the engine.

1 Like

Note: I understand the code snippet linked above isn’t supposed to explain how WaitForChild works.

Recently learnt this, and I think it could be useful for some people, and it should be another point as to why WaitForChild is cringe.

WaitForChild, unlike what some people think, doesn’t actually listen to events like .ChildAdded, .ChildRemoved, :GetPropertyChangedSignal("Name"), etc.

Well it could, but it is very unlikely, because the thread it’s called from seems to resume on Stepped, as far as I have tested.

If it did, behaviour wise it could be pretty different at some edge cases.

Thankfully though, having multiple calls running in the background doesn’t have much of a performance cost, this comes from it probably being a C function, etc.

Anyhow, I think this can be curious and useful to some people, so here’s a Luau representation of :WaitForChild, as far as I know, it should be mostly accurate to how it actually works. (Ignore the asserts, WaitForChild becomes even weirder when it comes to replicating its behaviour)

function Instance:WaitForChild(
    childName: string,
    maxSeconds: number?
): Instance?

    assert(
        typeof(childName) == 'string',
        "Must be string"
    )

    assert(
        maxSeconds == nil
        or typeof(maxSeconds) == 'number',

        "Must be number or nil"
    )
    
    local thread = coroutine.running()
    local timePassed = 0
    local connection
    connection = RunService.Stepped:Connect(function(deltaTime)
        timePassed += deltaTime
        if maxSeconds and timePassed >= maxSeconds then
            connection:Disconnect()
            task.spawn(thread)

            return
        end

        local child = self:FindFirstChild(childName)
        if child then
            connection:Disconnect()
            task.spawn(thread, child)
        end
    end)
    
    return coroutine.yield()
end

This should be functionally accurate with Roblox’s :WaitForChild.

This means that a :WaitForChild call will not be resumed immediately when a child with the requested name does exist, it will defer that until Stepped, on which if that child with that requested name no longer exists / has a different name / is in another location, it will ignore and continue yielding.

I ended up making a custom WaitForChild function that does use listeners and Janitor to achieve this, you can see it by clicking here if you wanna check out how that’s done.

Note:

On my Luau representation I preferred to not go with a while true do Signal:Wait() end because of performance concerns. :Wait has to create a linked list node for the RBXScriptSignal and doing that every frame can be a performance concern, more serious when there’s multiple calls running.

This way there’s not any memory being allocated for that linked list node, and on Immediate Signal mode, basically no memory allocation at all for that, and on Deferred, only one for the thread it creates every frame. (There could be some obscure memory allocation with the :FindFirstChild but other than that, I’m pretty confident)

Why did you mention me in here? :thinking:

Jokes aside, nice tutorial. It definitely helped me understand these parameters more.

Quick question, why are you using task.spawn() on the current thread it’s running? Shouldn’t it be using coroutine.resume() for resuming the thread?

Edit:
Quoting from the task library documentation

Accepts a function or a thread (as returned by coroutine.create) and calls/resumes it immediately through the engine’s scheduler. Arguments after the first are sent to the function/thread. This function does not return any value, even if the provided function returns one immediately.

Still, why task.spawn()? Any differences?

In coroutine’s documentation, it will act more like pcall. With task.spawn, you are getting an error in the output instead, so you can see potential errors if you do not have an error handler.