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

Addressing an earlier comment I made. Thanks for all the feedback up to now, I’m still in the process of testing around. Seems I have some misplaced information and new information.

There’s still a lot of threads out there on FindFirstChild and WaitForChild. This one is essentially a compilation of my own knowledge, information and testing. Still have a ways to go.


Added a bad habit for WaitForChild, 4. This one relates to the attempt of microoptimisation by indirectly using the DataModel’s WaitForChild (inherited from Instance), which is often confused for a regular optimisation. In short: you save barely anything noticeable, may miss bad uses of either function and may potentially affect readability.


After some lengthy discussion with @devSparkle above, I’m inclined to review the post as well as a few responses I’ve offered and correct them as such. I’d also like to find a better format for the thread if at all possible. I’ll see what I can manage soon.


This post:

No good. Dot syntax can be used on contents of ReplicatedStorage. This is because ReplicatedStorage’s contents are among objects that are replicated before DataModel.Loaded fires, which is when non-ReplicatedFirst scripts run.

There’s also a pretty good post you can reference that talks about avoiding WaitForChild by waiting for the game to load, even though LocalScripts don’t fire until that’s done anyway. Essentially: implicit (or more properly, static) objects can avoid the use of WFC, but instances added at run time (or more properly, dynamic objects) should incorporate WFC. See that here:

@Dandystan gave me this pointer.1

cc @superhudhayfa


1: But doesn’t want to post.

11 Likes

To sum it up nicely: Instance:WaitForChild is only necessary when accessing
(assuming Workspace.StreamingEnabled is disabled*):

  • Instances parented during runtime (including StarterGui to PlayerGui cloning).
  • Descendants of instances replicated to the client during runtime (including StarterGui to PlayerGui cloning).
  • Instances outside of ReplicatedFirst from a LocalScript inside ReplicatedFirst before DataModel.Loaded fires/DataModel:IsLoaded returns true.
  • If network streaming is enabled, BaseParts or descendants of BaseParts inside Workspace.

*Regarding network streaming’s effect on replication behaviour: I believe, when it is enabled, WaitForChild is also necessary when accessing descendants of Workspace. I am not 100% sure if anything else is affected.

6 Likes

As far as StreamingEnabled goes, WaitForChild is required for BasePart descendants of the Workspace only, since streaming pertains mostly to rendering. Parts streamed out effectively do not exist.

Thanks for the feedback though! I’ll be sure to weave something in for a future edit.

2 Likes

Hey there! I have a question, do you need to use WaitForChild on ReplicatedStorage from local scripts for instances you explicitly place there from studio, also, if you need to use it, can you use game.Loaded to allow direct access.

2 Likes

I think it was mentioned that the contents of ReplicatedStorage are replicated before LocalScripts execute and game.Loaded fires, so just indexing directly shouldn’t be a problem.

That is if you explicitly create the objects in Studio (for them to exist implicitly on the server), not the case for late instancing.

My source: @colbert2677

Provided the instance you’re trying to access exists there with the premade part of the game (the stuff you construct in Studio) and isn’t placed there by a script using Instance.new or anything, yes it’s okay to use dot syntax on the client.

1 Like

I pm’ed @Anaminus and he said that you can’t, base on your reply you made to another thread: Can I rely on ReplicatedStorage Instances loading before PlayerScripts run?, but maybe I asked the question incorrectly, If anyone else has an answer I would love to get your input.

1 Like

Here is the question I sent:
“Hi just clarifying that your post is saying that you can directly access static instances in replicated storage from a local script, aka you don’t have to use WaitForChild.”
Reply:
" That would be false. Objects visible to a LocalScript will always be on the client. Objects under ReplicatedStorage on the client will never be static, because they have to be replicated in from the server.

The only way you could access an object under ReplicatedStorage directly is if the object exists on the server before the client connects, and the LocalScript is guarded by game.Loaded ."

1 Like

The following thread, which was written by an engineer, better explains this dynamic in detail. Best practices around FindFirstChild and WaitForChild can be answered by this thread, but I wrote one that directly addresses those habits in respect to the DataModel.

LocalScripts do not need to use WaitForChild on instances in ReplicatedStorage that you explicitly place in from Studio, but it will need to be for objects that either the server or client creates during a running server (runtime). game.Loaded will not need to be used here since most LocalScripts are game.Loaded protected anyway (they do not execute until this fires).

I’m still doing replication tests myself and finding a time to polish the thread since it has a few inaccuracies and information to be covered, but the answer you got back is right. The server needs to replicate instances not created by the client that are eligible for replication. Premade parts in Studio will always be guaranteed available before a client connects. Most LocalScripts other than ones in ReplicatedFirst are protected from running until game.Loaded fires.

7 Likes

The post where I talk about static vs dynamic instances tries to figure out when it’s safe to use direct indexing. This was really a mistake; it is never safe to use direct indexing, though for reasons unrelated to replication. The following post has more detail:

It was also a mistake because it answers a misguided question. The question was “when is it safe to use direct indexing?”, and the answer is “when the instance exists”. The actual underlying question is “when does the instance exist?”, and the answer to that is “it’s complicated”.

Making these “static” and “dynamic” labels was also a mistake. We can’t take an abstracted approach to instance lifetime. The game tree must be understood as a dynamic data structure with instances being added and removed at any point in time.

There are several contexts in which instances will be present, or not exist. For example,

  • When studio opens a place file, the game tree will contain all the instances that were loaded from the file.
  • A server starts in much the same way, but is more dynamic because the game is actually running.
  • When a client starts, the game tree will contain basically nothing, because it has not yet received instances replicated from the server.
  • Certain instances will never be replicated over, depending on where they are in the tree.

There are a number of behaviors that will add and remove instances at particular points in time. Examples:

  • Replication will add and remove instances from a client’s game tree as needed to match that of the server.
  • When a client initially connects, one large snapshot of the server’s game tree is replicated over.
  • Instances can be created, destroyed, and moved by scripts, which themselves can be added and moved around.

It’s important to identify what behaviors are possible in a given context. Such as,

  • Replication does not occur in a non-running studio session.
  • Scripts in the game tree are also not running at this point.
  • On the other hand, plugins are running.
  • On the client, CoreScripts can start running before anything has even started replicating.
  • StarterScripts wont start running until the client has finished loading (game.Loaded).

Finally, there are a variety of APIs that are able to make certain guarantees about instances. To name a few:

  • Given a correct name, game:GetService() will always return the service corresponding to the name.
  • workspace.Terrain will always exist (“Terrain” is also a property of Workspace).
  • ChildAdded will always pass a newly added child.
  • Players.PlayerAdded will always pass a child that will be of the Player class.
  • If Player.Character is not nil, it is guaranteed to point to the player’s character.
  • FindFirstChild will return the first child instance of the given name, or nil if it does not exist.
  • Instance.new has the characteristic that the returned instance has absolutely no other references to it, so it is actually “safe” to use. A cloned instance tree also has this characteristic.
  • A reference to an instance stored in a variable or table will stick around. Don’t discard the reference if you’ll be using the instance later.

Such guarantees should be utilized as much as possible, because they do most of the work of locating an instance in a dynamic structure. If such an API does not exist, it is important to make (or find) a feature request for it, and rally support. It seems more clear now than ever that developers need some sort of API that is as convenient as direct indexing, but also as robust as FindFirstChild. Proposals for such have been made in the past, so they should be revisited.

24 Likes

A useful addendum to what @Anaminus has already outlined is: never trust direct indexing to do as you wish. In Roblox’s internal indexing hierarchy, properties will always be indexed first. I’m sure this is common knowledge but to this day (somehow) games still attempt to directly index. This is incredibly dangerous for properties you may not control. Take for example my username:

local Players = game:GetSerivce("Players")
local PlayerName = "GameAnnounce"

-- This code is fine, even though you could just fetch player from PlayerAdded
local Player = Players:FindFirstChild(PlayerName)
-- This code is not fine. This returns Roblox's event (https://developer.roblox.com/en-us/api-reference/event/Players/GameAnnounce)
local NotFinePlayer = Players[Player.Name]

-- This will always be false, even if my player is not in your game (RbxEvent == nil)
print(NotFinePlayer == Player) -- > false

A notable, yet unfortunate, example of this is Arsenal. By indexing players, and I assume other members, directly they open themselves up to bugs which they cannot foresee (within reason). I understand that this is a rare example, but a good one nevertheless to illustrate the dangers of direct indexing.

8 Likes

Just going to pop in here as noone has mentioned this, FindFirstChild and all forms of it can cause lag and can slow down your scripts depending on how many you are using.

2 Likes

Though there is a case that developers may miss which is the fact that an object’s properties have higher precedence than a child. In the case that a child instance shares a name with an object property, whether it’s a new or existing property, direct indexing will raise problems in your code.

Well, pretty negligible if you’re good with names though.

3 Likes

Always wondered this, why not just use a period instead of FindFirstChild?

if workspace:FindFirstChild("Part") then

end

sure, you can do that, but why not just do this?

if workspace.Part then

end
1 Like

It will error if the part doesn’t exist.

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.