Tables cannot be cyclic error

I’m trying to invoke the server to return data from a table on the server to the client. I’ve done this in the past but for some reason I’m getting an error that “tables cannot be cyclic” on the line where I invoke server. I feel like I’m missing something very blatantly obvious but I cannot figure out where the tables are cyclic.
83ce791832c33745b277af47248e8361

-- Module Script in ReplicatedStorage

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RemoteFunctions = ReplicatedStorage.Remotes.Functions
local PlayerData = RemoteFunctions.FetchData:InvokeServer("PlayerData") -- Where the error is occuring 

-- Server Script
RemoteFunctions.FetchData.OnServerInvoke = function(Player, DataType)
   print(DataHandler[DataType])  -- The table does print here, should return the table
   return DataHandler[DataType]
 end

The server is invoked when the module script loads in so that the player data is available on the client side.

1 Like

I’m not too familiar with the cyclic error, but would it be possible to show us more source for a deeper look? If you’re uncomfortable with sharing such, that’s alright.

1 Like

You have a reference to your table in your table which cannot be be sent over bindable events/functions. I can’t tell you exactly why, but I assume it has something to do with serialization issues.

I’m glad you said this. Upon a deeper look, commenting out everything in the table that’s being returned allows the InvokeServer to return the table.

Something inside of this PlayerData module is causing this error to occur.

Note: This module script is on the server and is what’s being returned when I return DataHandler[DataType].

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local ProfileService = require(ServerScriptService.ProfileService)

local HouseHandler = require(ServerScriptService.Main.HouseHandler)

local PlayerData = {
	Profiles = {},
	
	GameProfileStore = ProfileService.GetProfileStore(
		"PlayerData",
		{
			Location = "Residential",
			Level = 1,
			Gold = 0,
			Quests = {},
			Settings = {},
			HouseInfo = {
				HouseName = nil,
				HouseColor = {R = 13, G = 33, B = 77},
				AssignedHouse = nil,
				EquippedHouse = "House1"
			},
			Inventory = {
				Houses = {"House1"},
				Furniture = {},
				Pets = {},
				Food = {},
				Vehicles = {},
				Misc = {}
			},
			DailyReward = {
				CurrentDay = 1,
				Claimed = false
			}	
		}
	),	
	
	FetchData = function(self, Player)
		return self.Profiles[Player].Data
	end,
	
	LoadData = function(self, Player)
		local profile = self.GameProfileStore:LoadProfileAsync(
			"Player_" .. Player.UserId,
			"ForceLoad"
		)
		if profile ~= nil then
			profile:Reconcile() 
			profile:ListenToRelease(function()
				self.Profiles[Player] = nil
				Player:Kick()
			end)
			if Player:IsDescendantOf(Players) == true then
				self.Profiles[Player] = profile
			else
				profile:Release()
			end
		else
			Player:Kick() 
		end
	end,
	
	SaveData = function(self, Player)
		local profile = self.Profiles[Player]
		if profile ~= nil then
			profile:Release()
		end
	end,
	
	PlaceOverhead = function(self, Character)
		local Player = Players[Character.Name]
		local Overhead = ReplicatedStorage.Interface.Overhead:Clone()
		local PlayerHead = Character:WaitForChild("Head")
		Overhead.Parent = PlayerHead
		Overhead.Adornee = PlayerHead
		Overhead.Main.LevelFrame.Level.Text = self.Profiles[Player].Data.Level
		Overhead.Main.PlayerName.Text = Player.Name		
	end,
	
	AssignHouse = function(self, Player, Houses, InteriorHouses)
		local AssignedHouseId = HouseHandler:RemoveFirstVacantHouse()
		self.Profiles[Player].Data.HouseInfo.AssignedHouse = AssignedHouseId
		local AssignedHouse = workspace.ResidentialArea.Houses["HouseSpacer" .. AssignedHouseId]
		local AssignedInterior = workspace.ResidentialArea.InteriorHouses["InteriorHouseSpacer" .. AssignedHouseId]
		local EquippedInterior = ServerStorage.HouseInteriors[self.Profiles[Player].Data.EquippedHouse .. "Interior"]:Clone()
		
		Player.RespawnLocation = AssignedHouse.SpawnLocation
		AssignedHouse.Owner.Value = Player.Name
		AssignedHouse.Billboard.Owner.Text = Player.Name .. "'s Home"
		
		EquippedInterior.Parent = AssignedInterior
		EquippedInterior:SetPrimaryPartCFrame(AssignedInterior.HouseMatch.CFrame)
		HouseHandler:SetupResidentialTeleport(Player, EquippedInterior, AssignedHouseId)	
	end,	
	
	RemoveOwner = function(self, Player, Houses, InteriorHouses)
		local AssignedHouseId = self.Profiles[Player].Data.HouseInfo.AssignedHouse
		local AssignedHouse = workspace.ResidentialArea.Houses["HouseSpacer" .. AssignedHouseId]
		local AssignedInterior = workspace.ResidentialArea.InteriorHouses["InteriorHouseSpacer" .. AssignedHouseId]
		
		AssignedHouse.Owner.Value = ""
		AssignedHouse.Billboard.Owner.Text = "Vacant"
		HouseHandler:InsertVacantHouse(AssignedHouseId)
		
		for _, Model in pairs(AssignedInterior:GetChildren()) do
			if Model:IsA("Model") then
				Model:Destroy()
			end
		end
		
		self.Profiles[Player].Data.HouseInfo.AssignedHouse = nil
	end,
}

return PlayerData

This error occurs when you add a table to itself.

local a = {}
a.test = a -- Tables cannot be cyclic

I’m usually not very bright, and I don’t see how you would get such an error on that line since it doesn’t look like you’re adding anything there. Can I also see the full output?

2 Likes

Your exact issue seems to be occuring due to you referencing Server sided tables in the client-bound table, which is a security hazard, therefore this will throw a cyclic error and prevent this transfer.

EDIT: Thank you @JarodOfOrbiter for pointing out my ignorance on this topic. I think it’s time for some sleep, 26 hours can’t be good for me and it’s showing.

It looks like it’s invoking the server and trying to return the PlayerData, and when returning the PlayerData, something inside the PlayerData is causing this error to occur.

I doubt this is the issue considering that I’ve retrieved data from the server from the client before.

That’s not what this error means.

@InTheAMG I think your best course of action is to set a breakpoint or comment out the lines one by one. I’m not currently in the headspace to decipher someone’s code, especially when it is as intricate as yours. But if you can narrow down the location and then find a line that accidentally is adding a table to itself (or a table containing itself), you’ll have your solution.

2 Likes

ProfileService just has cyclic tables when you retrieve data. You’ll have to strip out any cyclic part of the table

2 Likes

Please provide info on what “DataHandler” is. This is a focused variable in your original post.

EDIT: @steven4547466 and @JarodOfOrbiter are much more knowledgeable on this subject than I am, please refer to them while I get some rest. Hope this issue gets resolved.

1 Like

This is the likely solution, considering that I replaced PlayerData with a different table and it worked fine. The only issue is now, even when I index a key in the table to strip out the rest of the table, it’ll still throw the error. I’ll need to figure a way to get the data some other way. Thanks!

If you give me a little bit, I might be able to come up with a way to remove all cyclic references in a table. But it may take a while to make.

That would be greatly appreciated. I do find it interesting, though, if I was to return PlayerData.Profiles I would still get the cyclic error. I assume it wouldn’t need to look through the table before referencing the Profiles key, no?

I’m not sure exactly why it has cyclic references, but even in my code that uses profile service it has them everywhere. I’ve just never needed to send it over a bindable before.

Alright I can’t tell you this is the most efficient way, but this is a way I figured out:

function removeCyclic(t, p)
	local t1 = {}
	for k in pairs(t) do
		if table.find(p, t[k]) then 
			continue 
		end
		if typeof(t[k]) ~= "table" then
			t1[k] = t[k]
		else
			table.insert(p, t[k])
			t1[k] = t[k]
			local nonCyc = removeCyclic(t[k], p)
			for k1 in pairs(t1[k]) do
				if not nonCyc[k1] then
					t1[k][k1] = nil
				end
			end
		end
	end
	return t1
end

To use this, you just do this
local noCyc = removeCyclic(tableToRemoveCycs, {tableToRemoveCycs})

Here is the script I used to test this:

local bf = Instance.new("BindableFunction")

bf.OnInvoke = function(x)
	print(x)
end

function removeCyclic(t, p)
	local t1 = {}
	for k in pairs(t) do
		if table.find(p, t[k]) then 
			continue 
		end
		if typeof(t[k]) ~= "table" then
			t1[k] = t[k]
		else
			table.insert(p, t[k])
			t1[k] = t[k]
			local nonCyc = removeCyclic(t[k], p)
			for k1 in pairs(t1[k]) do
				if not nonCyc[k1] then
					t1[k][k1] = nil
				end
			end
		end
	end
	return t1
end

local t = {}

t["a"] = 5
t["t"] = t
t["f"] = {}
t["f"]["b"] = 1
t["f"]["c"] = t["f"]
t["f"]["e"] = {}
t["f"]["e"]["x"] = 4
t["f"]["e"]["z"] = t["f"]["e"]
t["f"]["e"]["a"] = {}
t["f"]["e"]["a"]["y"] = 3
t["f"]["e"]["a"]["n"] = t["f"]["e"]["a"]
print(t)

local t1 = removeCyclic(t, {t})

print(t1)

bf:Invoke(t1)

so9lrf_92271

I can’t say whether it will work for every type of circular table or every depth of every circular table but I tested with a depth of 3 and I can tell you it worked with that.

I just made it into a module as well:

3 Likes

Great resource. Thank you for taking the time to make this, I’m sure many other developers will find this useful in the future!

2 Likes