This is an adaptation of a Twitter thread I made a while ago. This is simply a better formatted and more fleshed out version.
Let’s talk about wait()
. Not wait(n)
(although if you are using small n
values to where it’d be barely distinguishable from wait()
, you’re probably going to hit the same issues mentioned in this thread).
We probably learned about wait()
at first as the solution to “why are my infinite loops crashing me?”. It’s a super easy solution and it works! This starts a bad habit of just putting wait()
to fix your code.
In my opinion, wait() has no place in production code.
Common Usages of wait()
Let’s take this piece of code:
local flag = false
someAsyncThing(function()
-- bla bla bla
flag = true -- we're done!
end)
repeat wait() until flag
-- flag is ready!
The repeat wait() until
is being used to stop code execution until flag is true. This is known as polling (colloquially, you’ll also hear it called busy waiting in circles for higher level software, although they’re not technically the same). Polling in our case is bad because we’re unnecessarily waiting for something to happen instead of just doing code once it happens.
Consider this scenario:
-
flag
is set to false. -
someAsyncThing
calls the callback immediately with no yielding.flag
is now true. -
repeat wait() until flag
is ran. Despiteflag
being true, we’re still waiting for no reason. - Code continues.
“Ah, but wait!”, you cry, “I can do this!”
local flag = false
someAsyncThing(function()
-- bla bla bla
flag = true -- we're done!
end)
while not flag do
wait()
end
-- flag is ready!
Yes, you can. However, you’re still unnecessarily waiting even after flag
is set to true.
Consider this scenario:
-
flag
is false. - The callback to
someAsyncThing
isn’t ran right away. - “Is
flag
true? No? Alright, give me a bit.” -
flag
is set to true! Can our code run yet? - “No, sorry, still waiting.”
- “Is
flag
true? Yes, it is. Continuing code.”
Our code unnecessarily waited despite it being completely ready to run! The alternative is to simply hook your code up to an event.
If we assume someAsyncThing
will never call before the last line (Roblox Lua is single threaded so unless that’s a defined behavior, it won’t)…
local event = Instance.new("BindableEvent") -- You could use coroutines, but they're a bit finnicky
someAsyncThing(function()
-- bla bla bla
event:Fire()
end)
event.Event:Wait()
This is the exact same number of lines of code as the repeat wait() until
block. Let’s analyze this code again with the last scenario.
- The callback to
someAsyncThing
isn’t ran away, and it’s not documented that it can. - “Tell me when you’re ready for me to run”
-
someAsyncThing
’s callback is called. “Hey, I’m ready for you to run now” - We instantly get the message, and continue our code.
“But someAsyncThing
can be called immediately in my case!”
In that case, you can do its admittedly uglier cousin:
local event, called = Instance.new("BindableEvent"), false
someAsyncThing(function()
-- bla bla bla
called = true
event:Fire()
end)
if not called then
event.Event:Wait()
end
Again, this will be ran instantly. No unnecessary polling. Plus, this is milliseconds/seconds saved. Not measly microseconds.
“That’s for my own code, but what if I want to wait for some Roblox code to run.”
You mean like this?
repeat wait() until LocalPlayer.Character
Still no. Roblox has events for everything you could possibly be waiting for (and if it doesn’t make a feature request!). You can yield on an event by calling :Wait()
(no relation).
LocalPlayer.CharacterAdded:Wait()
This will yield the thread until the player’s character is added. And I know, I know, “what if the character already exists”? Feast your eyes on this beauty:
local character = player.Character or player.CharacterAdded:Wait()
Witnessing this level of elegance is dangerous to our feeble human brains. It gets the character and if it doesn’t exist, will wait until it’s added. It will be ran the instant the character is added.
Useful events to yield on in no particular order:
AncestryChanged
ChildAdded
ChildRemoved
CharacterAdded
RunService.Heartbeat
“I use wait()
because of a Roblox bug”
We’ve all been there. Sometimes there’s just some weird bug that wait()
seems to fix. Once you’re certain it’s a Roblox bug and there’s no event you can use, you still shouldn’t use wait()
. Have you considered:
RunService.Heartbeat:Wait()
or RunService.Stepped:Wait()
?
These are at least 2x faster than wait()
and I’ve never had an issue where they didn’t work but wait()
did.
“Okay, but why should I care?”
wait()
is code smell, meaning using it is the sign of a design problem. Polling is bad when event based programming is staring right at you. Not only is it a design problem, but it has its own issues. A major issue is that wait()
(and also wait(n)
) can wait for MUCH longer than you want it to. The more you call wait()
, the longer it will take. wait()
can easily last seconds, leading to a very laggy game experience. Take your pick on which of those is more important to you.
In summary: you don’t need wait()
, ever. :Wait()
works basically all of the time, and Heartbeat:Wait()
/Stepped:Wait()
fill in the gaps that aren’t filled.