Datastores: When returning nil in transformFunction, UpdateAsync returns nil rather than stored value

When you call this:

print(datastore:UpdateAsync("TestKey", function(oldValue) (...) end))

No matter what the code in the transformFunction is, the UpdateAsync call should always return the value stored at that key at that moment in time, so the latest value of the key should be printed.

However, if the code inside the transformFunction returns nil, this indicates that the update should be cancelled, which is indeed what happens, but the UpdateAsync call also returns nil! This would falsely indicate that the current value of the key is nil, which is not the case.

How to reproduce:

  1. Publish a baseplate to a game with Studio API access.
  2. Run the following code in the command bar:
data = game:GetService("DataStoreService"):GetOrderedDataStore("Test")

data:SetAsync("TestKey", 42)

local retValue = data:UpdateAsync("TestKey", function() return nil end)
print("return value of UpdateAsync: " .. tostring(retValue))

local storedValue = data:GetAsync("TestKey")
print("current value of TestKey: " .. tostring(storedValue))
  1. Observe output.

Observed behavior:

return value of UpdateAsync: nil
current value of TestKey: 42

UpdateAsync should return the “value of the entry in the data store with the given key”, but instead returns nil, while the key wasn’t updated to nil, it’s still 42! (as it should be, the return value of UpdateAsync is just wrong here)

Expected behavior:

return value of UpdateAsync: 42
current value of TestKey: 42
5 Likes

This is still an issue now so I’m kicking back this topic for developers to be aware of it and hopefully to garner engineer attention unless it’s more sound to write a new bug report.

I think it would be incredibly valuable for this issue to be fixed. I recently helped a team write a system involving UpdateAsync to avoid write conflictions across different sessions and having the existing value returned if nil is returned would be very helpful to avoid performing a write when I don’t want to or using silly strategies I shouldn’t like extracting the value or using another GetAsync.

The current behaviour is not consistent with documentation and it crops up the need to write absurd workarounds. I had to write a monstrosity to this extent because of this issue:

local a, b = getABNonYielding()

datastore:UpdateAsync("__KEY", function(old, keyInfo)
    if old then
        a, b = old.a, old.b
        return nil
    else
        return a, b
    end
end)

If this issue were fixed, I would be able to write this instead:

local infos, keyInfo = datastore:UpdateAsync("__KEY", function(old, keyInfo)
    if old then return nil end -- Cancel write and give me the current entry
    local a, b = getABNonYielding()
    return {a = a, b = b}, {}, {} -- Write and give me what I just wrote
end)

An additional point: if returning nil to cancel an update, UpdateAsync will instead return three values, the exact ones you feed to UpdateAsync, instead of a value and a DataStoreKeyInfo:

local dataStore = game:GetService("DataStoreService"):GetDataStore("__TEST")
print(dataStore:UpdateAsync("__TEST", function(old, keyInfo)
    return nil, {}, {foo = "bar"}
end)) --> nil, {}, {foo = "bar"}

* My specific use case is dynamic dedicated server creation. Developers would be able to add any places to the experience and the server would handle creating a dedicated server for that place upon the first time it is ever joined by any visitor (the idea is locking Roblox automatic matchmaking from creating more than one instance of a place).

4 Likes