Datastore function errors when fired once, but works fine in a loop

I grabbed the tutorial datastore from the roblox wiki, and was fiddling around with it to make it work for me. However the function they use in the normal script only works when its looping. I tried to place it outside the loop, and it errored. What was even stranger is that when i fire the function once while in another script its being fired every 2 seconds, it does work. The error is ServerStorage.PlayerStatManager:21: attempt to index field '?' (a nil value)

Modulescript in serverstorage

local PlayerStatManager = {}
 
-- Create variable for the DataStore.
local DataStoreService = game:GetService('DataStoreService')
local playerData = DataStoreService:GetDataStore('PlayerData')
 
-- Create variable to configure how often the game autosaves the player data.
local AUTOSAVE_INTERVAL = 60
 
-- Number of times we can retry accessing a DataStore before we give up and create
-- an error.
local DATASTORE_RETRIES = 3
 
-- Table to hold all of the player information for the current session.
local sessionData = {}
 
-- Function the other scripts in our game can call to change a player's stats. This
-- function is stored in the returned table so external scripts can use it.
function PlayerStatManager:ChangeStat(player, statName, changeValue)
	sessionData[player][statName] = sessionData[player][statName] + changeValue
end
 
-- Function to retry the passed in function several times. If the passed in function
-- is unable to be run then this function returns false and creates an error.
local function dataStoreRetry(dataStoreFunction)
	local tries = 0	
	local success = true
	local data = nil
	repeat
		tries = tries + 1
		success = pcall(function() data = dataStoreFunction() end)
		if not success then wait(1) end
	until tries == DATASTORE_RETRIES or success
	if not success then
		error('Could not access DataStore! Warn players that their data might not get saved!')
	end
	return success, data
end
 
-- Function to retrieve player's data from the DataStore.
local function getPlayerData(player)
	return dataStoreRetry(function()
		return playerData:GetAsync(player.UserId)
	end)
end
 
-- Function to save player's data to the DataStore.
local function savePlayerData(player)
	if sessionData[player] then
		return dataStoreRetry(function()
			return playerData:SetAsync(player.UserId, sessionData[player])
		end)
	end
end
 
-- Function to add player to the sessionData table. First check if the player has
-- data in the DataStore. If so, we'll use that. If not, we'll add the player to
-- the DataStore.
local function setupPlayerData(player)
	local success, data = getPlayerData(player)
	if not success then
		-- Could not access DataStore, set session data for player to false.
		sessionData[player] = false
	else
		if not data then
			-- DataStores are working, but no data for this player
			sessionData[player] = {Money = 0, Experience = 0}
			savePlayerData(player)
		else
			-- DataStores are working and we got data for this player
			sessionData[player] = data
		end
	end	
end
 
-- Function to run in the background to periodically save player's data.
local function autosave()
	while wait(AUTOSAVE_INTERVAL) do
		for player, data in pairs(sessionData) do
			savePlayerData(player)
		end
	end
end
 
-- Bind setupPlayerData to PlayerAdded to call it when player joins.
game.Players.PlayerAdded:connect(setupPlayerData)
 
-- Call savePlayerData on PlayerRemoving to save player data when they leave.
-- Also delete the player from the sessionData, as the player isn't in-game anymore.
game.Players.PlayerRemoving:connect(function(player)
	savePlayerData(player)
	sessionData[player] = nil
end)
 
-- Start running autosave function in the background.
spawn(autosave)
 
-- Return the PlayerStatManager table to external scripts can access it.
return PlayerStatManager

script in serverscriptservice

-- Require ModuleScript so we can change player stats
local PlayerStatManager = require(game.ServerStorage.PlayerStatManager)
-- After player joins we'll periodically give the player money and experience
game.Players.PlayerAdded:connect(function(player)
	PlayerStatManager:ChangeStat(player, 'Money', 5)
	PlayerStatManager:ChangeStat(player, 'Experience', 1)
end)

This is most likely happening because you are calling the ChangeStat function before the setupPlayerData function has finished, meaning that sessionData[player] hasn’t been set yet.

You should probably change the ChangeStat function to something like:

function PlayerStatManager:ChangeStat(player, statName, changeValue)
	while(not (sessionData[player] and sessionData[player][statName]))do
		wait(1)
	end
	sessionData[player][statName] = sessionData[player][statName] + changeValue
end

or:

function PlayerStatManager:ChangeStat(player, statName, changeValue)
	if(sessionData[player] and sessionData[player][statName])then
		sessionData[player][statName] = sessionData[player][statName] + changeValue

		--Successfully set stat
		return true
	end

	--Session data didn't exist
	return false
end
1 Like

sadly this did not fix it. when i used your first suggestion it just went into an infinite loop. i think the problem is somewhere else, but idk where.

Then I’d suggest adding some prints to the setupPlayerData function, to see if it actually behaves like you intended.

A quick look suggests you’re running into an issue with your ChangeStat connection firing before your setupPlayerData connection either finishes or fires, thus causing an nil value error as the data hasn’t had a chance to be setup. What I recommend is either putting a wait(2) before using changeStat, and if it fixes, implement playerStatManager:waitForDataReady(Instance rbxPlayer) which will yield the calling thread until sessionData[rbxPlayer] exists

this both doesnt seem to fix it :frowning:

I guess i have to write a datastore system entirely from scratch

I just tried running your original scripts in a Studio Test Server with the ChangeStat function changed to my first example, and everything seems to work as expected.

Before running the code, I also added some prints to the ModuleScripts:

Changed ModuleScript
local PlayerStatManager = {}
 
-- Create variable for the DataStore.
local DataStoreService = game:GetService('DataStoreService')
local playerData = DataStoreService:GetDataStore('PlayerData')
 
-- Create variable to configure how often the game autosaves the player data.
local AUTOSAVE_INTERVAL = 60
 
-- Number of times we can retry accessing a DataStore before we give up and create
-- an error.
local DATASTORE_RETRIES = 3
 
-- Table to hold all of the player information for the current session.
local sessionData = {}
 
-- Function the other scripts in our game can call to change a player's stats. This
-- function is stored in the returned table so external scripts can use it.
function PlayerStatManager:ChangeStat(player, statName, changeValue)
	while(not (sessionData[player] and sessionData[player][statName]))do
		wait(1)
	end
	sessionData[player][statName] = sessionData[player][statName] + changeValue
	print("Incremented",statName,"by",changeValue,"for",player)
end
 
-- Function to retry the passed in function several times. If the passed in function
-- is unable to be run then this function returns false and creates an error.
local function dataStoreRetry(dataStoreFunction)
	local tries = 0	
	local success = true
	local data = nil
	repeat
		tries = tries + 1
		success = pcall(function() data = dataStoreFunction() end)
		if not success then wait(1) end
	until tries == DATASTORE_RETRIES or success
	if not success then
		error('Could not access DataStore! Warn players that their data might not get saved!')
	end
	return success, data
end
 
-- Function to retrieve player's data from the DataStore.
local function getPlayerData(player)
	return dataStoreRetry(function()
		return playerData:GetAsync(player.UserId)
	end)
end
 
-- Function to save player's data to the DataStore.
local function savePlayerData(player)
	if sessionData[player] then
		return dataStoreRetry(function()
			return playerData:SetAsync(player.UserId, sessionData[player])
		end)
	end
end
 
-- Function to add player to the sessionData table. First check if the player has
-- data in the DataStore. If so, we'll use that. If not, we'll add the player to
-- the DataStore.
local function setupPlayerData(player)
	print("Setting up data for",player)
	local success, data = getPlayerData(player)
	print(player,success,data)
	if not success then
		-- Could not access DataStore, set session data for player to false.
		sessionData[player] = false
	else
		if not data then
			-- DataStores are working, but no data for this player
			sessionData[player] = {Money = 0, Experience = 0}
			savePlayerData(player)
		else
			-- DataStores are working and we got data for this player
			sessionData[player] = data
		end
	end	
end
 
-- Function to run in the background to periodically save player's data.
local function autosave()
	while wait(AUTOSAVE_INTERVAL) do
		for player, data in pairs(sessionData) do
			savePlayerData(player)
		end
	end
end
 
for _,plr in pairs(game.Players:GetPlayers())do
	setupPlayerData(plr)
end

-- Bind setupPlayerData to PlayerAdded to call it when player joins.
game.Players.PlayerAdded:connect(setupPlayerData)
 
-- Call savePlayerData on PlayerRemoving to save player data when they leave.
-- Also delete the player from the sessionData, as the player isn't in-game anymore.
game.Players.PlayerRemoving:connect(function(player)
	savePlayerData(player)
	sessionData[player] = nil
end)
 
-- Start running autosave function in the background.
spawn(autosave)
 
-- Return the PlayerStatManager table to external scripts can access it.
return PlayerStatManager

After running the code this was showed in the output:

image

Maybe something else is causing your problem.

I made a barebone version of what im trying to do and put it in an uncopylocked place.
Maybe then we can figure out whats wrong because you can replicate it.

Are you getting the error on live servers as well?

For Studio testing, you should loop through all players before connecting to PlayerAdded, like this:

for _,plr in pairs(game.Players:GetPlayers())do
	setupPlayerData(plr)
end
-- Bind setupPlayerData to PlayerAdded to call it when player joins.
game.Players.PlayerAdded:connect(setupPlayerData)

yes, getting the same error on live servers. if it did work in live servers i wouldnt post here :stuck_out_tongue:

Then replace your code with my slightly changed version, and send a screenshot of the server logs after pressing the part.

Your modified modulescript just makes it yield forever :frowning:

And there is nothing in the output?

nope, nothing at all.

Because you aren’t requiring the PlayerStatManager module until someone presses the part, the PlayerAdded event will never fire for your player.

To solve this, either require the module when the server starts and/or modify the PlayerAdded section of the module like suggested earlier:

2 Likes

I added this, and i cant thank you enough. this finally solved my problem!!

Awesome! Glad you got it working :smiley:

2 Likes