Please do keep the following in mind before reading or linking!
Some parts of this thread provide slightly inaccurate, unnecessary or outdated information. You are strongly encouraged to read the replies for further discussion about FindFirstChild and WaitForChild and their use in code, as well as some pointers on replication.
I have not yet found a time to correct potential misconceptions in this thread. It does not provide authoritative advice and you should always double check information with peers.
Additional note (as of May 10th, 2023):
A lot of the issues presented in this thread can be relatively easily resolved with Deferred SignalBehavior. I will make sure to mention this during the future rewrite, which to date I still don’t know when I will get to it. Please refer to the above and discussion in comments for more information.
Being an avid reader of the Scripting Support category, I read a lot of other developers’ code on what’s a less-than-hourly basis. Through this, I’ve discovered a bad pattern amongst Roblox developers regarding the uses of FindFirstChild and WaitForChild. By extension, it’s safe to say that the pattern plagues any other similar methods, such as FindFirstAncestor.
This thread seeks to discuss these methods, habits around them and propose appropriate alternatives to them. Adhering to new idioms is up to you.
FindFirstChild
FindFirstChild is a method you’ll commonly come across for Roblox objects. The function’s purpose is to, as its name states, find the first child of an object that matches a given parameter. In FindFirstChild’s case, it is a child with a case-sensitive name you pass like so:
object:FindFirstChild("CASE_SENSITIVE_NAME")
FindFirstChild supports a second argument, which is a boolean value. This determines whether the function should be called recursively on the children of the object. By default, not including this argument or marking it as false will disable it.
Enabling recursion will call FindFirstChild on all children and their children until the criteria is met or not. FindFirstChild with recursion enabled is essentially FindFirstDescendant, but that method doesn’t exist.
FindFirstChild also has similar equivalents, to which the practices of FindFirstChild can apply towards.
-
FindFirstAncestor: The same as FindFirstChild, except it will check the name of a parent and its parent until it reaches the DataModel.
-
FindFirstAncestorOfClass: FindFirstAncestor, except searches by the ClassName property instead of the Name property. ClassNames, like FindFirstAncestor, are case sensitive and locked to a singular type of class.
-
FindFirstAncestorWhichIsA: FindFirstAncestorOfClass, except it runs IsA and checks for a truthy return rather than checking if ClassName matches the passed argument. Can be used for abstract and base classes. For example, you can pass BasePart to this method, but not to FindFirstAncestorOfClass.
-
FindFirstChildOfClass: Same as FindFirstAncestorOfClass, but for children. Does not support recursion, so using the WhichIsA variant may prove more useful.
-
FindFirstChildWhichIsA: Same as FindFirstAncestorWhichIsA, but for children. Does support recursion if you’re interested in descendants over children.
Bad Habits
There are a number of bad habits practiced with FindFirst methods. Some of them you may not have known or some of them you may be very familiar with seeing.
Bad Habit 1: Throwing away what FindFirstChild returns.
Chances are that if you have a callback function, you’re depending on the function to give you something back after it finishes executing. A bad habit to develop is to throw away that value. There are cases when value-throwing is acceptable but for FindFirstChild, not so much. You’re looking for a different method of indexing children if so.
What constitutes as throwing away a variable?
Simply: not using what the function returns. If you keep the return value in a variable and use it later, you aren’t throwing it away. Here are two prominent examples of not throwing away the value:
-- Using it as a means of checking for existence
if character:FindFirstChild("FireParticle", true) then
print("Character is on fire - do something!")
end
-- Creating a backup if the original doesn't exist (existence check x2)
local repository = storage:FindFirstChild("Libraries")
if not repository then
repository = Instance.new("Folder")
repository.Name = "Libraries"
repository.Parent = container
end
The chances of actually having this bad habit in your code are very slim. At the very most, as it relates to FindFirstChild, variable throwing just hampers readability and could potentially produce an error. In addition, you always want to pick options that have less dependencies.
I won’t comment on performance because it’s negligible and I don’t want anyone to start panicking about optimisation; doesn’t matter here.
-- I've seen code like this. Bad idea, especially if using recursion argument.
-- This code depends on place in hierarchy.
if character:FindFirstChild("FlameParticle", true) then
-- If FlameParticle is not directly under the character, this will error.
character.FlameParticle.Enabled = true
end
-- This is more readable. No? It also supports recursion if you enable it.
-- This code only depends on an object. Place in hierarchy is irrelevant.
local FlameParticle = character:FindFirstChild("FlameParticle", true)
if FlameParticle then
FlameParticle.Enabled = true
end
Bad Habit 2: Expecting and assuming a truthy value is always returned.
This is perhaps one of the most common bad habits when using any of the child find methods. Developers often assume that the functions always return a truthy value. They do not.
The beauty of a callback is that it allows you to handle return values on a case-by-case basis. If the functions fail to find an object based on the specified criteria (according to the function you choose as well as the arguments you pass), nil will be returned.
Often I have seen the following done:
local objectAB = workspace:FindFirstChild("AB")
objectAB:Clone()
workspace:FindFirstChild("CD").Position = Vector3.new(0, 0, 0)
Both of these are wrong and misuses of the function. Both of them will error and state that there was an attempt to index a nil value. Specifically:
- First code: attempt to index local ‘objectAB’ (a nil value)
- Second code: attempt to index a nil value
These functions are capable of returning nil values, meaning your criteria could not be met. It’s cases like this that you need to set up an existence condition. Depending on the context of your code, it may be necessary to handle or ignore a nil return, a non-nil return or both.
Code Samples
-- Handling non-nil returns
local objectEF = workspace:FindFirstChild("EF")
if objectEF then
objectEF.Position = Vector3.new(0, 0, 0)
end
-- Handling nil returns
local objectGH = workspace:FindFirstChild("GH")
if not objectGH then
print("GH was stolen!")
end
-- Handling both cases (variation 1)
-- If this is more readable to you, use this over V2.
local objectIJ = workspace:FindFirstChild("IJ")
if objectIJ then
print("IJ is secure.")
else
print("IJ is lost!")
end
-- Handling both cases (variation 2)
-- If this is more readable to you, use this over V1.
local objectKL = workspace:FindFirstChild("KL")
print(objectKL and "KL in map." or "KL not in map.") -- Fake ternary
print(("KL %s map"):format(objectKL and "is" or "isn't")) -- Fake ternary
Bad Habit 3: Using FindFirstChild in place of WaitForChild.
Not something I see often or at all, but still a bad habit: FindFirstChild is not an alternative to WaitForChild. They are two separate functions for a reason. WaitForChild itself will be discussed further into the tutorial.
Some old legacy character scripts code had a custom implementation of WaitForChild. I’m not sure if WaitForChild existed at the time, but if it did, this seemed fairly pointless. It reinvents the wheel for what already exists. Core scripts have since been corrected and they all use vanilla WaitForChild.
For those curious on that implementation, here is the exact code that was used. For clarity’s sake: do not use this. This is merely for knowledge’s sake.
function waitForChild(parent, childName)
local child = parent:findFirstChild(childName)
if child then return child end
while true do
child = parent.ChildAdded:wait()
if child.Name==childName then return child end
end
end
Appropriateness
As the name of each function suggests, it is used to find the first object which matches a certain criteria specified by the developer. Typically, it’s appropriate to use these functions in cases where you need to check for an object’s existence immediately on-call. There isn’t much else you will need it for.
WaitForChild
WaitForChild is another fairly common search method in code. The function’s purpose can also be explained by its name: it will wait until an object matching the specified criteria exists. WaitForChild will pause threads until the criteria is met or the the timeout is reached, which is set by the developer with the second argument.
WaitForChild does not have any equivalents and it does not support recursion. It only supports waiting for a child by a given name, the first argument, a string.
Bad Habits
It’s very common to take up bad habits with WaitForChild moreso than the FindFirst methods.
Bad Habit 1: Using WaitForChild inappropriately.
WaitForChild is fairly inefficient as a method itself and there are recommendations from other developers and the Developer Hub only to use it when necessary. There are relatively few cases where you need to use WaitForChild.
Case 1: Accessing outside of ReplicatedFirst from ReplicatedFirst.
As the name suggests, ReplicatedFirst is the first container that the server replicates to the client. Its LocalScripts are also allowed to run before DataModel.Loaded fires. DataModel.Loaded determines when the server has finished replicating the initial snapshot of the game to the client. It is thus possible for ReplicatedFirst code to execute before an object it needs has been replicated.
Case 2: Accessing the contents of PlayerGui outside of PlayerGui.
The contents of StarterGui are cloned into PlayerGui when the character loads. In the case of a custom spawning solution where CharacterAdded doesn’t fire, PlayerGui’s contents remain empty. If you have code in StarterPlayerScripts, this code will run quicker than the server has a chance to add contents to PlayerGui. This time window is then necessary to have WaitForChild.
Case 3: You’re waiting for an instance to be copied to the character.
Self explanatory. This also goes if you’re running code from StarterCharacterScripts, since the order in which objects are cloned is not guaranteed or known to you.
Case 4: Late instancing.
If you instance an object later but want your LocalScripts to wait until said object has been instanced, then WaitForChild is what you’re going to be searching for. Not much to this point.
Case 5: Non-implicit instances.
Instances are not implicit on the client-side, as the server must replicate instances to it. On the flip side, there may be server-side instances that are not implicit either. For example, a game loop script you add in Studio to ServerScriptService is implicit, but the Lua Chat System which gets added at run time and then its contents reparented is not implicit. Other than non-implicit instances or late instanced objects, otherwise you should never be using WaitForChild on the server.
With the above cases in mind, there is something to say; with WaitForChild, despite it being a callback, it’s not a bad practice to throw away the variable it returns depending on what specifically you’re trying to do. You can equate it to calling RBXScriptConnection.Wait.
Bad Habit 2: Using WaitForChild as a replacement for FindFirstChild
FindFirstChild exists for a reason. WaitForChild has a built-in call to FindFirstChild, but you should not take advantage of that fact and present unnecessary overhead in your code. Use the proper method, depending on the context of your code.
Bad Habit 3: Expecting and assuming a truthy value is always returned (case dependent).
Just like FindFirst, WaitForChild is also capable of producing a nil value, however this only goes if you specify a timeout via its second argument. If you don’t have a timeout, WaitForChild will wait permanently until criteria is met. If you have a timeout, it waits that amount of time before returning nil due to not being able to find an object.
In terms of WaitForChild without a timeout, a truthy value is guaranteed and thus you can call methods or index properties directly after a call.
workspace:WaitForChild("MN"):Destroy()
On the other hand, if you have a timeout, you cannot do this. Remember the behaviour of WaitForChild with a timeout: if it reaches the timeout without the criteria being fulfilled, it will return nil.
-- Will error if OP does not exist within 1 second
workspace:WaitForChild("OP", 1):Destroy()
Personally, regardless of the circumstances, I wouldn’t call methods directly on what WaitForChild returns. Often this doesn’t look readable and doesn’t fit in line with my use cases for WaitForChild. So long as you recognise what you can’t do to avoid errors (which is only one case in this section), then it’s up to you what you choose to adopt within your code.
Bad Habit 4: Using DataModel.WaitForChild.
Developers are often very bent on trying to write optimised code. In many cases, developers don’t properly understand the concept of microoptimisation or lead into grounds of microoptimisation. As it relates to WaitForChild, I have seen the DataModel’s WaitForChild being put into a variable to “increase performance” of the method call.
local WaitForChild = game.WaitForChild
local BasePlate = WaitForChild(workspace, "Baseplate")
This isn’t so much of a bad habit for WaitForChild specifically but it goes moreso for readability. If this readable for you, then there’s no problem in using it. On the other hand, it’s important to understand that this is simply a microoptimisation and you gain virtually nothing from it. I prefer to write clear and concise code, hence using the method on the actual object over indexing DataModel.WaitForChild.
local BasePlate = workspace:WaitForChild("BasePlate")
Appropriateness
Since this section was mostly discussed by Bad Habit 1, there’s not much use in repeating it. To summarise what that section discussed; there are rare cases where you need WaitForChild. So unless you think it’s necessary to wait for a child that may not exist, don’t use it.
With that, I bring this resource to a close. I hope you were bored enough to read this and look in awe about bad practices with FindFirst/WaitForChild, as much as I was to even think about writing this.
I’ve been writing this thread for around three days now and question why I haven’t just posted it. I’m tired and can’t think of any other cases, so this is what I’m presenting. This thread will be updated given time and feedback.
Please do leave feedback if you disagree with what I’ve said, feel like making a comment or you have something extra to add. Obviously people won’t agree with me since practice is fairly subjective when it comes to the aforementioned two methods, up until error-bound code appears.
Happy developing.