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.