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

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.

369 Likes
Is this overdoing it with WaitForChild()?
Is :WaitForChild() tied to legacy like wait()?
How do I know when UI objects are done loading and can be referenced?
MouseButton1Click Only works in studio
ObjectValues referenced break script, don't know why
Can I rely on ReplicatedStorage Instances loading before PlayerScripts run?
Keeping all GUIs in ReplicatedStorage then cloning them?
When is it appropriate to use WaitForChild?
Studio/ Scripting Questions
I'm a newbie to scripting, are there any bad habits or good habits I should be building?
WaitForDescendants()
WaitForDescendants()
Should I use WaitForChild for module scripts?
When do I use waitforchild, indexing workspace makes me so nervous!
How to make the player's waist move to the camera direction!
Problem locating a Folder in Workspace:FindFirstChild(player.Name)
Be careful when using assert() and Why
Error in output, but Script works fine
Custom character script is inefficient?
Why my Tool ability stop working when i clone to backpack from replicated storage
When do I use waitforchild, indexing workspace makes me so nervous!
FindFirstChildWhichIsA returns nil?
My best practices for roblox dev (mostly on the technical side)
Is :WaitForChild():WaitForChild() Bad Practice?
Replication to server is fine, but it's not appearing on client...?
Script tells me to attempt to index nil with color even tho it just a second ago used it just fine
If-Clause for Team
Best Scripting Practices?
My scripts seem to fail
The difference between WaitForChild and FindFirstChild
Having trouble understanding FindFirstChild and WaitForChild
Is Variable faster than BaseValue?
SoundGui help in scripting!
Why is this simple damage script not working?
Introducing colbert2677, Hobbyist Developer and Platform Enthusiast
How could I improve or clean this code?
"WaitForChild" & "FindFirstChild" , when to use?
Referencing Player Names
Detect if a player have a tool named "Hamburger" after a click event
Need help on if statement
Gui tweening doesn't work

This is awesome!!!

As someone who is trying to both learn to become better at scripting more complex things, but also trying to learn how to be a more efficient scripter this is an awesome guide. I’m definitely going to reference this in the future!!

9 Likes

This is so great, because whenever someone new to scripting starts learning, he is never told about “optimization” and what’s bad and what’s good, he just goes on learning more but with least knowledge about knowing how efficient his script is

15 Likes

What a great PSA!

I’d like to leave with a few pointers of my own, and also slightly refute the notion that WaitForChild is the rotten apple in the barrel. As far as I recall, there are three ways to go about indexing child objects within RbxLua. These can be summarized in the following categories:

Direct Indexing
This type of indexing is perhaps the first type you will learn when you’re starting out in Lua. It’s a no-frills immediate check for a particularly named object; it’s instant, quick, and leads to errors when the object doesn’t exist.

Code Examples
workspace.Terrain
-- OR
workspace["Terrain"]
-- OR
local Name = "Terrain"
workspace[Name]

Indirect Indexing
This is what you would call your average FindFirst... function. It allows you to retrieve a child or a descendant through a safe method, that will not error if the object does not exist. It has a larger overhead than Direct Indexing, but it allows you to target index using the object’s ClassName or inherited classes.

Code Examples
workspace:FindFirstChild("Terrain")
-- OR
workspace:FindFirstChildWhichIsA("Terrain")

Yield Indexing
You’ve reached the depths of Lua. Users’ beware! The (un)yielding swamp monster of indexing functions lies ahead! This is of course, your WaitForChild function. This function has a substantially larger overhead, while allowing you to suspend the operation of the current thread (yield) until such time as the requested object is a descendant.

Code Example
workspace:WaitForChild("Terrain")

Shiver me timbers! What shall ye’ do with so many methods at your disposal? Let’s break it down now ya’ll.

All the child indexation methods have a good purpose and appropriate usage to them. While various developers will come up with arbitrary rules of when to use them, I find the following guidelines appropriate to select a good method:

Direct Indexing is best used when an object must be present at all required times. You want the script in question to error if the object doesn’t exist.

Indirect Indexing probably has the most varied range of use cases. It can be very useful when you are expecting circumstances where the object may not exist, and you want to deal with these accordingly.

It’s also very useful when you want to index objects based on their class, or index an indirect descendant without creating a custom search function.

Yield Indexing is a great tool if you need an object to be present to proceed, but are expecting it to not exist yet. This arises in several cases, but most commonly then:

  • The object will be created by another thread.
  • The object will be replicated.
70 Likes

Another excellent guide from colbert. A detailed and informative read, and very appreciated. Thank you.

I find myself guilty of throwing away what FindFirstChild returns in rare cases, so thank you for pointing that out! Luckily that’s all I’m guilty of!

7 Likes

Very useful guide, I will try to watch out for these bad habits in my own code in the future.

image
This local script exists inside my gun tool. Are you suggesting there are better ways and habits to replace this over-use?

6 Likes

Great tutorial! I’ve always been an avid user of WaitForChild, especially in some unnecessary cases, and this will help me optimize my game further.

1 Like

Good read, as always! :smile:

I’ve seen different people using WaitForChild or FindFirstChild when it comes to Remotes and Bindables in ReplicatedStorage. Wondering if ReplicatedStorage loads before ServerScriptService - if it does so, I’ll stick with my FindFirstChilds. :wink:

3 Likes

Yeah. Typically in the case of a Tool’s LocalScript, it is an object that also exists with the other children of the tool. To prevent any potentially unwanted cases, you only need to call WaitForChild on top parents.

The only necessary cases of WaitForChild in your code are for the Handle and the Neon variables. The rest are all unnecessary cases.

When WaitForChild completes on the handle, we know our LocalScript acknowledges the Handle’s existence. And since cloning and replication are synchronous, the children of Handle will be available when Handle itself is.

The same would go for an outside script accessing your tool. That script would only need to use WaitForChild on the tool, then every other case becomes unnecessary.

24 Likes

EDIT: Nearly this whole post is misinformation. See this post:


This much is fine, since the contents of ReplicatedStorage aren’t implicitly available to the client, the server replicates this at the beginning of a session. It’s necessary to wait only for direct children of ReplicatedStorage, everything else can be indexed directly.

Don’t rely on the order of replication to help you determine what’s appropriate because you may run into cases where that dependence on order presents you with edge cases. Your code should always strive not to have dependencies where not required.

As for ServerScriptService, it has no business replicating to the client and anything you put there is implicitly available to the server when it starts. In short: it’s instantaneously available. So to put it into concept, technically ReplicatedStorage does not load before ServerScriptService.

The thing for ServerScriptService is that if you don’t include a script there in Roblox Studio, then it’s not implicitly available. For example, the Lua Chat System - in your Explorer, you don’t see ChatServiceRunner. This is why most code you see waits for ChatServiceRunner but doesn’t for a child module of it, ChatService.

local ServerScriptService = game:GetService("ServerScriptService")

local ChatServiceRunner = ServerScriptService:WaitForChild("ChatServiceRunner")
local ChatService = require(ChatServiceRunner.ChatService)

If ChatServiceRunner was already available in your game’s hierarchy and didn’t need to be cloned or reparented at run time (current method for injecting CoreScripts), WaitForChild would not need to be used on any of this.

Another example - suppose your game had a Bindables folder in ServerStorage. The server does not need to call WaitForChild to access it, it can do so directly by using dot syntax on it’s name.

local Bindables = ServerStorage.Bindables

FindFirst and WaitForChild are heavily case-dependent methods to use. The circumstances and context of your code will almost always determine whether or not these functions are appropriate to use. Especially of WaitForChild.

8 Likes

So for clarification sake, say you had a folder dedicated to Remotes and Bindables that was directly parented under ReplicatedStorage, you’d only need to use WaitForChild with that instance and for the instances parented under the folder, you only need to do Folder.ChildName?

Exactly. On the server however, ReplicatedStorage’s contents exist implicitly, meaning you don’t need to use WaitForChild at all on the server. You can use dot syntax for items in ReplicatedStorage however, since they’re replicated and available to LocalScripts by the time they execute.

11 Likes

Thanks for the detail. I’ve just seemed to develop this habit as I got too many errors from directly indexing in the PlayerGui, and carried it through to most of my scripts.

This is a great resource that I will definitely come back to in the future

1 Like

If this principle was to be true, wouldn’t it require that all children of the Tool, including Handle must be available when said Tool is replicated?

The main catch is where you’re calling the method from. A script’s place in hierarchy, especially that of a LocalScript since that’s what you’re likely working with when it comes to a Tool, changes the requirements.

I may require some clarification as to what you’re asking if the following does not answer your question, but I have done a bit of testing around with this theory and have produced some results.

Tool LocalScript

As expected, when a LocalScript is a child of the tool, it is also part of the items on the replication queue for the Tool. Therefore, you still need to call WaitForChild on any of the Tool’s immediate children, as they may be replicated later than the LocalScript.

In a quick test that was ran in Roblox Studio, part of this could be debunked. A Tool’s Handle is implicit to a LocalScript, as are its children. However, any other item part of the Tool is not implicit. Therefore, direct indexing will produce an error. Observe the following image from a repro:

As you can see, Event does exist in our tool and I attempted to index it with dot syntax. However, it produced the error that the Event isn’t a valid member. Therefore, Event has not been replicated at the time that the LocalScript was executed, which is why it’s not recognised as a member.

This proves the need to wait for top parents in a tool, aside from the Handle. For the sake of consistent conventions though, I personally recommend also waiting for the handle. Just like I mentioned in the post below it: you should not rely on the order of replication to save you from an index error.

External LocalScript

For an external LocalScript, I simply put a quick script in StarterPlayerScripts to test this. My repro produced results as expected and I will explain.

In the repro, all that’s necessary is to wait for the Backpack. Since my tool is placed into StarterGear, it gets cloned into the player’s Backpack when they spawn. As cloning is synchronous, the Tool’s children are immediately available when the Tool is parented.

Given that Backpack isn’t immediately added to a Player after their object has been instanced, I call WaitForChild on the Backpack. However, this gets rid of the necessity to use WaitForChild on the tool, so that’s an even further bonus. Observe how no error is produced in either occasion:

So essentially; the Tool instance itself may be replicated, but its children not at the same time. Therefore once you’ve waited for and retrieved a top parent, all of the Tool’s children will be available, including the Tool’s Handle post-replication or post-cloning.

6 Likes

What I was trying to note is that the fundamental assertion of your previous post, where if a child is replicated, so must it’s descendants is untrue.

Nevertheless, there is another point that must made. There is no issue with using WaitForChild if you are only calling it once to preload a variable. To expand on this, it’s completely fine to use WaitForChild to retrieve and store references to descendants of your tool. It is, however, very problematic when you use it repeatedly to obtain a an Instance that you’ve interacted with previously.

2 Likes

I find this to be contradictory to what’s been tested and how either replication or cloning works. Given the tests created, the children of an object are available in the case of an object being replicated. The code’s placement in hierarchy is fairly important to understanding this behaviour.

I’ve included above examples that also show this behaviour. In our case, we only call WaitForChild on a single parent to where an object is held and then any subsequent indexing via a method or dot syntax does not throw an error.

Do you have a source, reference or repro of some kind that’s able to disprove what’s been said or tested, or am I still not understanding your stance of disagreement?

On this one, while I do agree, it’s not something I recommend to others or practice myself. In the case of the presented code example to which I initially responded to, the removal of WaitForChild would result in a negligible performance gain and only truly has any relevance in readability and organisation.

I use WaitForChild as little as possible and only in cases where I would legitimately need it, hence unnecessary. You can do it, but it’s fairly pointless. To bring back an example,

local ServerScriptService = game:GetService("ServerScriptService")

local ChatServiceRunner = ServerScriptService:WaitForChild("ChatServiceRunner")
local ChatService = require(ChatServiceRunner.ChatService)

-- vs

local ServerScriptService = game:GetService("ServerScriptService")

local ChatServiceRunner = ServerScriptService:WaitForChild("ChatServiceRunner")
local ChatService = require(ChatServiceRunner:WaitForChild("ChatService"))

-- vs

local ChatService = require(game:GetService("ServerScriptService"):WaitForChild("ChatServiceRunner").ChatService)

--vs

local ChatService = require(game:GetService("ServerScriptService"):WaitForChild("ChatServiceRunner"):WaitForChild("ChatService"))

All of these accomplish the same thing and the only relevance between them is how they look in your code. I choose the one that uses less cases of WaitForChild since it looks more readable to me and I know that ChatService will be available when its parent, ChatServiceRunner, has been sent to the ServerScriptService.

1 Like

This isn’t a question of being readable, if the code is prone to failure. Server-client replication is subject to massive behavioral changes when under conditions such as:

  • Poor client performance.
  • Poor server performance.
  • Connection latency.
  • High instance count.

Furthermore, it’s imprudent at best to rely on behavior which is not documented, and is subject to changes in the future.

1 Like