It’s a hassle to deal with race conditions when working with synchronous code processes or code processes that read values/properties/variables outside of a function’s environment
The only real workaround developers have is to either write a custom yield function or tack on a repeat loop, both of which contain something akin to:
repeat task.wait() until x end
example:
---pretend that data is loaded/handled in modulescript "DataModule"
---this is a weak example but it should help get the point across
local function LoadItems(Player: Player)
if not DataModule.Cache[Player] then
repeat task.wait() until DataModule.Cache[Player]
end
local SavedData = DataModule.Cache[Player]
--... do things with saved items in SavedData
end
…it’s reminiscent of how old-school scripts had a wait(2) on line 1 to prevent dependencies from breaking
a cleaner, less-tacky solution to this problem would be:
task.yield<T>(condition: () -> T) -> T
task.yield<T>(value: T) -> T
it behaves similarly to assert(), except condition/value is “tried” until it returns/becomes something truthy, then yield will return that truthy value
I’m unsure if the 2nd overload is possible because it’s unknown how variables are handled under-the-hood, but value would be treated like a pointer to the variable being “tried”
---pretend that data is loaded/handled in modulescript "DataModule"
local function LoadItems(Player: Player)
local SavedData = task.yield(DataModule.Cache[Player]) --uses the 2nd overload in the proposed request
--... do things with saved items in SavedData
end
This doesn’t functionally work without significant extensions to Luau’s semantics supporting some kind of macro system: task.yield(DataModule.Cache[Player]) is the same as task.yield(nil) because the arguments you’re passing to a call get evaluated before that call gets a chance to do anything.
I would recommend looking into promise libraries, they can get you the semantics you want here.
This would make it extremely easy to write very confusing code and code that is harder to statically analyze.
I think the right solution to your problem is having your systems expose signals that you can wait on, and going to task.wait() loops if the implementation cost of that would be too high.
In the OP’s case, something akin to…
local function loadSavedData(player: Player)
if DataModule.Cache[player] == nil then
repeat
DataModule.CacheLoaded:Wait()
until DataModule.Cache[player] ~= nil
end
return DataModule.Cache[player]
end
Encountering race conditions is a sign that you need to look into defensively architecturing your codebase and rely less on timing. Some libraries that can help here are Promise, Future and Rx (via Observables).
Waiting at the top of a script to avoid race conditions is a code smell. Race conditions indicate an architecturing problem. Defensive architecture (e.g. via the aforementioned libraries, events, etc) will get you where you need to be without introducing bloat and noob traps in the API.
This most definitely would need a different name, because coroutine.yield exists but means something completely different, so reusing the name like this would not be nice. task.await could make sense, but yeah it’s very close to writing a loop manually.
As aforementioned, typically you’ll never experience race conditions in a better codebase. You should instead implement a callback function into “DataModule”, that is called whenever the data cache was loaded.
Just to note I don’t actually structure my code this way, and only wrote it as an example case
Although the race condition issue is still the same
Yeah, this is what I was worried about when writing this request
The only reason that kept me hopeful is that certain constructors that appear to have an optional value will throw errors when nil is passed, namely Random.new(nil) will throw an error but Random.new() won’t (despite arg Seed being annotated as optional), so I thought that the constructor was doing some non-Lua things with the Seed argument that was passed
I’m currently writing a Luau library that has modules that complement some of the the existing Luau libraries (Vector3, CFrame, math, etc) and when I have to reference it (which is a lot, as the library is functionally a swiss army knife), I always find myself having to do a “setup ritual” of sorts, where I’m basically pasting this at the beginning of every script:
local RS = game:GetService("ReplicatedStorage")
local Shared = RS:WaitForChild("Shared")
---... other definitions
--- @module src.Packages.LuauLib --RobloxLSP emmylua annotation
local LuauLib = require(Shared.LuauLib)
I’ve seen in other languages (and vanilla Lua as well) that libraries can be imported with a string reference, and although this probably isn’t possible because of the explorer architecture, it would be nice to have some way to effectively cut out the “setup ritual”
This is the closest thing to what I have in mind… and from what I know, if this was implemented I think it would be a lot cleaner than what’s currently available
Would adding task.await upend/have conflicts with the entire task library?
The lore behind the feature request is that I sometimes find myself referencing non-Instances with repeat spam and Instances via WaitForChild() chains or which both feel equally awful when I know that a better alternative may be possible
The Promise library pretty much fits the request/case to the T, but perhaps some of its functionalities can be implemented into the task library?
local x = task.yield(someTable.Key) -- or task.yield(someTable, "Key")?
… seems to be much less tacky than
local x = nil
-- repeat task.wait() until someTable.Key
repeat
task.wait()
until someTable.Key
x = someTable.Key
The better answer here is making it easier to distribute and consume 3rd party libraries for common patterns like promises and maids. That way it’s almost as convenient but you still have the flexibility to tweak those libraries if necessary rather being stuck with whatever behavior we chose for a builtin.
That’s something that we’re working towards solving.