Player:LoadCharacter() inconsistently errors when player leaves while it is running

Reproduction Steps

  1. Call Player:LoadCharacter()
  2. Have the player leave while Player:LoadCharacter() is being ran on the engine-side
  3. Observe the error LoadCharacter: Failed to apply character appearance, player not in world. The error only occurs some of the time with the repro place file below:

SpammingRespawnReproduced.rbxl (33.4 KB)

(Thank you @aaron_mccoy for finding the repro)

Expected Behavior
I’m not exactly sure if LoadCharacter() spitting an error when a player leaves is expected behavior or not. If it is expected / intended behavior, the error message should be more explicit to convey what exactly triggered the error in the first place. The current error message is very confusing and implicit.

If it isn’t expected, would the player’s character simply be parented to nil while it’s still being built by the engine?

Actual Behavior
LoadCharacter() spits an error if the player leaves while it is being executed on the engine side.

Workaround
pcall()ing Player:LoadCharacter()

Issue Area: Engine
Issue Type: Other
Impact: Moderate
Frequency: Sometimes
Date First Experienced: 2021-10-24 00:10:00 (-04:00)

14 Likes

Thanks for the report! We’ve filed a ticket to our internal database and we’ll follow up when we have an update for you.

2 Likes

Just chiming in to say that we are still experiencing this issue in our game. Thread here: "Failed to load character appearance, player not in world" even after checking player.Parent == Players beforehand - #14 by Superslammin

Any updates on a fix?

I think that this should error, but have documentation on the wiki regarding the error.

1 Like

Update, here is the code we’re currently using. If the player logs out during LoadCharacter() there is no success or failure and nothing after the pcall in the script runs:

if player.Parent == game.Players then
	print("Loading character")
	local success, message = pcall(function()
		player:LoadCharacter()
	end)
	if success then
	    print("Loaded character")
	else
		warn(message)
	end
else
	print("Player offline, skipping setup")
	continue
end

I thought the pcall would let the script continue to run if the function inside of it fails but that does not seem to be the case with player:LoadCharacter().

The issue seems to be entirely with players:LoadCharacter() stopping the thread if a player logs out during it. It also prevents the pcall from returning and anything after it from executing.

Easy to reproduce with either of the following code blocks. Log out and relog a few times until it stops running, which you’ll know because you won’t respawn anymore. Happens within a few attempts. Sometimes the pcall properly returns the error message, but sometimes it completely stops the thread:

while true do
	for _,player in ipairs(Players:GetPlayers()) do
		player:LoadCharacter()
	end
	wait()
end

or

while true do
	for _,player in ipairs(Players:GetPlayers()) do
		local success, message = pcall(function()
			player:LoadCharacter()
		end)
		if not success then
			warn(message)
		end
	end
	wait()
end

As a temporary work around I’m running player:LoadCharacter() within a coroutine but I’m hoping this can get resolved soon. Thank you!

1 Like

This is still a problem. I was able to reproduce it with the following steps:

  1. Open the repro place file (attached below)
  2. Run a test server with 1 connected client
  3. Click on the button in the bottom left of the screen. Observe that the character is reloaded, and all output is displayed in the console.
  4. Click the button again, but close the client window immediately (disconnect the client) before the character can be reloaded. Observe that the last line of output was never displayed by the server. The thread is infinitely yielded / was terminated.

image

LoadCharacter_InfYield_Repro.rbxl (35.6 KB)

I think the expected behavior here is that LoadCharacter returns a bool, describing whether or not the load was successful. If a player leaves during the operation, it returns false.

1 Like

I was able to recreate this issue with the reproducable.
Used the repro file in play test and happened on my first disconnect.

image

I have used both pcall and the Parent == Players checks like @Superslammin does in my live game, and still get this error sometimes:

my code:


-- Respawn player characters at end of each round.
for i,player in ipairs(Players:GetChildren()) do

    local statusCodeBoolTF,theErrorOrFirstOfAllReturnsFromTheCall = pcall(function() 
        task.spawn(
            function()
                player:LoadCharacter()
            end
        )  
    end)
     
end

1 Like

Still bugging for me as well, multiple sanity checks don’t resolve the split second issue.

This error is happening in my game as well: if people leave right when it’s trying to load their character it will trigger the error “player not in this world” , breaking my player manager script and stopping anyone else from being able to spawn. I may try wrapping player load character in a coroutine to see if it that helps in the meantime, not sure if anyone else had success with that.

The current workaround is just using a pcall (atleast that works for me). Would be great if we had a proper solution though.

in this thread it seemed like numerous people were saying not even the pcall reliably worked- how is yours setup? spawn function?

Tested the repro in the reply and it’s still an issue. IMO LoadCharacter should throw an error if the player leaves during the process, but any solution is better than silently infinitely yielding.

Here’s my workaround:

function PlayerService:LoadCharacter(player: Player, timeout: number?): Model?
    local functionThread = coroutine.running()

    local asyncThread = task.spawn(function()
        local previousCharacter = player.Character
        pcall(player.LoadCharacter, player) -- Sometimes yields, sometimes doesn't
        task.spawn(functionThread, previousCharacter ~= player.Character and player.Character or player.CharacterAdded:Wait())
    end)

    task.delay(timeout or 10, function()
        if coroutine.status(asyncThread) == "suspended" then
            task.spawn(asyncThread)
        end
    end)

    return coroutine.yield(functionThread)
end
1 Like

We ended up needing to make our own wrapper too, yeah