:WaitForDescendant()

As a Roblox developer, it is currently too hard to make sure items are properly waited for in a secure way when needed - particularly if this involves many items.

If Roblox is able to address this issue, it would improve my development experience because I could save significantly on bloat while maintaining a secure structure for my game.

We’ve all been there - adding a bunch of stuff to workspace, expecting the client to have it and all of its descendants, and finding out that it didn’t load completely by the time the client called for it.

Our current solution is either to make sure in another way (Usually preferred), or to use :WaitForChild.

Only problem is, most of the time I have many items I want to wait for to make SURE that there are no gaps in my code. My possible solution to that, if deemed acceptable, is :WaitForDescendant.

It would work simply - if I wanted to wait for Folder → A → B → C, I would do Folder:WaitForDescendant(“A”,“B”,“C”).

This is significantly cleaner, and would save a lot of code and time as compared to
Folder:WaitForChild(“A”):WaitForChild(“B”):WaitForChild(“C”).

This is simply an issue I’ve run into a lot. Sure, I can make my own function to do it. But it’s a common enough occurrence for me that I feel it would be a welcome addition. Thoughts?

27 Likes

This API would conflict a bit with the current (yet not enabled iirc) FindFirstDecendant API which attempts to find the first decendant of a particular name, rather than defining a specific hierarcy. This could possibly create confusion between the functions and what they accept, too.

6 Likes

This is a valid argument. I do think that defining the hierarchy would be more practical, here - perhaps a different naming scheme is in order.

WaitForHierarchy could work? Although that doesn’t specifically imply waiting to fetch the last descendant down the line, which would still be a nice feature.

2 Likes

Why not just make one yourself?

function WaitForDescandants(Root: Instance, ...: string)
	local Path = {...}
	table.insert(Path, 1, Root)
	for i = 2, #Path do
		Path[i] = Path[i-1]:WaitForChild(Path[i])
	end
    return Path[#Path]
end

--Usage:
local HRP = WaitForDescandants(workspace, "Guest9132342", "HumanoidRootPart")

took me like 3 mins to make this, pop it into a utility module nad ur good.

I have a bunch of these in my utilty module

4 Likes

Of course that’s an option, but it’s definitely more convenient to have it already made as a built in function. Take this for example. You can use this following code to replace :WaitForChild, but that doesn’t mean :WaitForChild shouldn’t be a thing:

local function WaitForChild(inst, name)
	local temporaryVariable -- im too lazy to come up with a proper variable name
	
	repeat
		temporaryVariable = inst.ChildAdded:Wait()
	until temporaryVariable.Name == name
	
	return temporaryVariable
end

I haven’t tested this script so don’t kill me if there’s a small mistake lol

5 Likes

Oh no it’s certainly something that can be coded easily - but that means using a utility module (If you want to get the best use out of it), which you’d have to require. This is a very small thing, but again it could be super nice to have integrated considering how often I’d use it.

I imagine a lot of scripts that are in starterplayer that want to use the utility module would want to use :WaitforDescendants just to require the module itself too, lol.

2 Likes

Deferred signals and observer patterns help significantly with the problems you’re facing (especially with avoiding daisy chaining WaitForChild) to the point where this API isn’t needed. Can’t say I agree with the feature request, sounds like it’d be a noob trap. Dependence on timing isn’t good.

4 Likes

The WaitForChild API should have never been added; and there is not a day that goes by, which I do not wish for its swift deprecation. While I acknowledge the method’s comfortable placement in the standard practice, it does not–nor will it ever, appear in my code. A sufficient solution to any problem which might otherwise be solved with :WaitForChild, can always be produced using the existing events. This proposed API is as unnecessary, as it would be nightmarish in its implementation details.

Edits for additional context:
I figure the wording of this reply could be improved. To be clear, WaitForChild is notoriously perhaps the single most misused method in the API. While there are rare cases in which its utilization is good and proper, there’s far more in which its (all considering) exceedingly niche logic, is irrationally preferred, by virtue of its approachable presence in the API. I do not believe that the slight convenience of having this occasional one-liner, is enough to justify the evils inherent in its rampant misapplication; and so I opt to avoid it altogether. Needless to say, the feature request in subject, in addition to its apparent flaws, would be counterproductive to these issues.

4 Likes

“make one yourself” is not really a good fix considering everything a user does in Lua can be made a lot faster from the C-side, and there will be no need of some third-party module, otherwise everyone would have been making their own “master”-modules with every single function it.

Not only that, it’s simply a lot more convenient to just :WaitForChild, instead of downloading some module (firstly, you need to find it too), putting it into your game and requiring it, or making a code snippet yourself that may turn out slow or faulty.

No need to re-invent the wheel.

4 Likes

If it won’t appear in your code, doesn’t mean it won’t appear it others, one of the best examples for that “API” is the “ChatSystemEvents” folder in ReplicatedStorage which is created by Roblox’s internal scripts and does not appear immediately, which means you have to wait for it.

Or, there are tons of ways when you have to wait for an object on client that is made by the server and vice-versa, or when it’s just made by another script, and you should also account that Roblox runs scripts randomly and there’s no “exact” order for them.

There are a lot of other different ways, but I’m not in the mood to explain every single one of them.

5 Likes

Because it’s REALLY slow (test it for yourself: make your own version of WaitForChild and benchmark it against the native roblox’s one)

4 Likes

This reply got me curious, in your mind, what do you think Instance:WaitForChild() should be used for then?

The only usage where I used WaitForChild was to use it as a trigger event or just for referencing guis in LocalScripts

2 Likes

Probably not as much of a scathing review as @AskWisp but it is a bit frustrating to use for two reasons:

  1. The internal listener doesn’t get cleaned up when an object is destroyed due to Roblox wanting to support legacy behaviour, discussed in this engine bug report here and here, e.g.:
--[!] Will yield for 5s despite being destroyed
local part = Instance.new('Part')
task.delay(1, part.Destroy, part)

local started = os.clock()
local something = part:WaitForChild('Something', 5)

local elapsed = os.clock() - started
print(string.format('Result: %q | Time yielded: %.2fs', tostring(something), elapsed))


--[!] Will throw an infinite yield warning despite being destroyed
local destroyable = Instance.new('Part')
task.delay(1, destroyable.Destroy, destroyable)

local item = destroyable:WaitForChild('Something')
print('We will never get to this point....')
  1. There doesn’t seem to be a method of being able to cancel or cleanup the listeners, even if you cancel/close the thread that started the listener. There’s some ambiguity as to whether it’s just the internal timer that’s not cleaned up, or whether the waiting thread is also leaked, e.g.:
--[!] Will still send a warning of `Infinite yield possible on 'Workspace:WaitForChild("SomePart")'`
local thread = task.spawn(function ()
	local someExpectedObject = workspace:WaitForChild('SomePart')
end)

task.defer(task.cancel, thread)


I know OP doesn’t want alternatives, but for reference:

Roblox uses their AtomicBinding library to achieve the desired behaviour OP discussed, found here and utilised within the RbxCharacterSounds script seen here.

Alternatively, I usually just utilise promises, as well as a maid class for cleanup, e.g. Ozzypig’s maid.

For example, if I wanted to support the desired API that OP has described…

Example method using a Promise
local Maid = require(some.maid.library)
local Promise = require(some.promise.library)

----------------------------------------------
--                                          --
--                  METHOD                  --
--                                          --
----------------------------------------------

--[=[
  Creates a promise that resolves with the asset when found,
  or rejects if the parent is no longer accessible.

    - Can be yielded using `::await()`
    - Can be timed out using `::timeout(n: number)`

  e.g.

    expectAsset(workspace, 'SomePart')
      :andThen(function (obj)
        -- do something
      end)
      :catch(function (e)
        -- handle error(s)
      end)


  @param parent Instance -- root node of the asset path
  @param ... string -- variadic arguments of type `string`
  @return Promise
]=]

local function expectAsset(parent, ...)
  local t = typeof(parent)
  if t ~= 'Instance' then
    return Promise.reject(string.format('Expected instance for parent, got %q', t))
  end

  local asset = parent
  local length = select('#', ...)
  for i = 1, length, 1 do
    if not asset then
      break
    end

    local name = select(i, ...)
    t = typeof(name)
    if t ~= 'string' then
      return Promise.reject(string.format(
        'Expected string for path element, got %q at index %d in Path<%s>',
        t, i, table.concat(table.pack(...), '.')
      ))
    end

    asset = asset:FindFirstChild(name)
  end

  if asset then
    return Promise.resolve(asset)
  end

  local path = table.pack(...)
  return Promise.new(function (resolve, reject, onCancel)
    local maid = Maid.new()

    onCancel(function ()
      maid:cleanup()
    end)

    local function progress(fn, ...)
      maid:cleanup()
      fn(...)
    end

    maid:addTask(
      parent.Destroying:Connect(function ()
        progress(reject, string.format(
          'Parent<%s> destroyed before finding asset in Path<%s>',
          parent and parent.Name or 'UNKNOWN', table.concat(path, '.')
        ))
      end)
    )

    maid:addTask(
      parent.DescendantAdded:Connect(function ()
        -- [!] Note:
        --      could optimise this to start observing the next child instead of the root node

        local object = parent
        for i = 1, length, 1 do
          if not object then
            return
          end

          object = object:FindFirstChild(path[i])
        end

        progress(resolve, object)
      end)
    )
  end)
end

Example usage of the method
----------------------------------------------
--                                          --
--                   USAGE                  --
--                                          --
----------------------------------------------

local function someErrorHandler(result)
  result = typeof(result) == 'string' and result or 'Unknown error occurred:\n' .. tostring(result)
  warn(result)
end

-- e.g. yielding
local success, result = expectAsset(workspace, 'SomeModel', 'SomePart'):await()
if success then
  print('Got:', result)
else
  someErrorHandler(result)
end

-- e.g. yielding with timeout
local success, result = expectAsset(workspace, 'SomeModel', 'SomePart'):timeout(1):await()
if success then
  print('Got:', result)
else
  someErrorHandler(result)
end

-- e.g. as a promise, and an example of cancelling the waiting thread & listeners
local cancellable
cancellable = expectAsset(workspace, 'SomeModel', 'SomePart')
  :andThen(function (object)
    -- do something with the part

  end)
  :catch(error)
  :finally(function ()
    if cancellable and cancellable:getStatus() == Promise.Status.Cancelled then
      warn('We were cancelled!')
    end
  end)

-- e.g. example cancellation of the promise to clean up listeners
task.delay(1, function ()
  if cancellable and cancellable:getStatus() == Promise.Status.Started then
    cancellable:cancel()
  end
end)

-- e.g. promise with a timeout of some arbitrary time like 2seconds
expectAsset(workspace, 'SomeModel', 'SomePart')
  :timeout(2)
  :andThen(function (object)
    -- do something with the part

  end)
  :catch(function (e)
    if Promise.Error.isKind(e, Promise.Error.Kind.TimedOut) then
      warn('We timed out after 2s of waiting for workspace.SomeModel.SomePart')
      return
    end

    -- e.g. otherwise some other error occurred
    return error(e)
  end)

2 Likes

I believe — I may be wrong — that this is the intended workings of WaitForChild?


From my experience, you can add children to destroyed instances; The deleted instances are still “usable” in a way

That’s correct, yeah, as in my post:

with reference to:

what do you use as a replacement for “:WaitForChild” ?