Remove the " Something unexpectedly tried to set the parent of X to NULL while trying to set the parent of X"

Because you are wrong, sorry. Even if your premise that wait() was designed to do something is correct, you’re still wrong because wait() was designed to yield. If it didn’t yield when you gave it 0, that would be unexpected behavior.

1 Like

waiting 0 seconds (0 seconds = no time) not yielding for an amount of time is unexpected behavior
what

1 Like

Well then I expect wait(0.1) to wait exactly 0.1 seconds. Why doesn’t it.

local x = calculateSomething()
wait(x)

So you’re telling me that this code should change behavior entirely if x is 0?

Because of hardware, internal workings of software, etc that’s not possible.

No. The current behavior should stay the same – the current behavior is like it is because of hardware, internal, etc restrictions. But what you shouldn’t do is rely on that obscure implementation quirk (unless there isn’t another option) – that’s bad design.

If I want to play a ROBLOX sound multiple times, I have to clone it and play the clone because if I played the original multiple times the previously playing instances would get stopped as soon as I played the next one. For the sake of example, let’s say I use a cleaner that loops through all the cloned sounds, checks if they’re stopped, and deletes them if they are. For whatever reason, I need to trigger the cleaner manually. I can do this by cloning a random sound and immediately stopping it, or I can write and use a method for the cleaner that triggers it manually. Which code should I use?

...
local clone = reloadSound:Clone()
clone:Play()
clone:Stop()
...
...
SoundCleaner:Run()
...

With the way the cleaner is implemented, I can certainly trigger it manually by cloning an arbitrary sound and stopping it, but is that good code? If someone was reading that besides me, they would have no idea what it was for unlike if I made a method for the cleaner. I’m no expert, but I thought the general consensus of programmers was that good code is inherently understandable and that needing to follow a trail of method sources and observing what each one does just so you can find out what the top level is doing is bad.

1 Like

When scripters use wait() they are intending for their code to pause for that amount of seconds. The name of the function being “wait” implies this…to pause or wait that time.

This is the implementation. Usually when you want something to be done, the implementation isn’t always as simple, but it was designed to let you force your code to wait a period of time given in seconds. Of course this isn’t decimal perfect simply because thats extremely hard to achieve due to technical reasons, but its pretty damn close. The expected behavior of wait(0) is to wait 0 seconds or no time. This makes no sense. So obviously the behavior it gives is unexpected.

ROBLOX’s default wait-time is ‘0.029999999999999999’ wait(0) is the same thing as wait() and last I checked ROBLOX discouraged from using wait(). wait() was intended to yield but when you give it a number that makes no sense and ROBLOX has to instead resort to the default wait-time then thats just bad practice.

4 Likes

Oh, completely forgot about that. wait(0) isn’t even possible because the minimum wait time is 0.03:

start = tick() wait(0) print(tick()-start)
-> 0.031400680541992

So yeah, you should definitely be using spawn to resume at the next scheduler resume instead of using wait(0) and picking back up at the next resume after 0.03 seconds.

2 Likes

t=tick()spawn(function()print(tick()-t)end)
0.01389741897583

?

waiting 0 yields 1/30th of a second. Spawning yields until the next resume at < 1/60th of a second, its expected behavior.

I hate wait(0) as much as the next guy but this has nothing to do with the OP.

However, how this argument started is a good idea - you could avoid this error by simply doing this:

~~~
spawn(function() part:Destroy() end)
~~~
6 Likes

Why are we even talking about wait? The problem here is logical. If you change the parent while you’re executing an AncestryChanged event then what do you do with the connections that haven’t been fired yet?

part.AncestryChanged:connect(function()
   part.Parent = workspace
end)

This will error because you’re trying to change the data that fired the event while the event is still firing. wait() isn’t a magic solution that makes the error not happen, it yields the thread and allows the event to continue executing so that your code will change the data after the event finishes.

10 Likes

If this is the reason why this happens then can’t there be a check to see if any unfired connections exist? If so, finish them then destroy? If theres no connections (As in most cases) just destroy.

2 Likes

That would basically turn Destroy into a yieldfunction and thus changes the behaviour/expectations that you should have of the method, I don’t think that’s a good idea. Yielding yourself before calling Destroy seems a lot cleaner, then you are actively aware of the yielding, instead of it being a potential side effect.

3 Likes

Because no one expects Destroy to execute other lua code that changes the state of the game. What we really need is a way to manually yield the thread so that those other connections can execute.

Ah, wait()

1 Like

coroutine.yield() would work better in this case. It’s basically wait() with no resume delay.

1 Like

To be specific, the code everyone is looking for here is:

coroutine.yield(coroutine.running())

Which will add the current thread back into the “execution queue”, but still for the current frame, to be executed after all other currently queued threads are executed. Basically, it will “wait” until all other threads for the current frame have completed, but not actually “wait” any frames (as will normally be the case if you use the “wait()” function). You would use it something like this:

game.Workspace.ChildAdded:connect(function(child) 
    coroutine.yield(coroutine.running()) 
    child:Destroy()
end)

Which would successfully destroy the part without it existing for any frames.

4 Likes

running() isn’t necessary, though. yield() already pauses the calling thread in its own, and the scheduler doesn’t do anything with the arguments passed to yield().

Zeuxcg described this as a really ugly bug that shouldn’t be utilized.
I wouldn’t advise using this @PlaceRebuilder, as this might get patched eventually.

1 Like

He meant that you at least shouldn’t use it for loops, because it means you execute several times per frame until the time quantum of the Lua scheduler runs out (so you literally fill up the remainder of the time quantum with your task, several times).

It’s not as ugly if you just call it once to put the coroutine back into the queue for the current frame temporarily. Not sure if the fact that it processes it again in the same cycle is the intended behaviour though.

This scheduling issue should be fixed btw.

3 Likes