Stop using SetAsync() to save player data

I have to agree with @Rocky28447 on this one. I do not see how UpdateAsync prevents data loss at all when it comes to saving player data.

I believe that most people experiencing data loss with SetAsync have horrible coding structure and logic, which of course will result in players losing data. I’d really like to see what you changed your code from and to to see the difference.

One way players can lose data is if developers wanted to update players’s data but ended up having a mistake in their code, which resulted in it replacing the actual saved data.

The only benefit (that I can see and make it worth putting up here) UpdateAsync has is in case you are updating a key with some new data often, where it will retrieve the latest updated data and return it so you can modify it in the way you like.

If anyone has valid reasons as to why UpdateAsync is better to use than SetAsync, then please inform me as it would be in the interest of everyone to know this.

Also:

Another possible reason why developers say DataStores is difficult, is due to Roblox’s horrible data stores. You never know when it may fail or not, and you don’t know if the data you just retrieved was the player’s actual data, or, if what you just tried to save, actually saved.

18 Likes

Sure, UpdateAsync() used by itself would act the same as SetAsync(), however when utilised fully provides two huge benefits:

  1. It doesn’t overwrite previous data if nil is returned - If your function somehow messes up then it won’t wipe the player’s data.
  2. It considers the old value before making changes . This enables you to implement a value which is changed each time data is saved (the incremental value). As @colbert2677 mentioned, UpdateAsync respects previous data and conflicting write requests.

When GetAsync() fails and returns a false/nil value, then you are correct, the error can be handled appropriately using either method. However, when GetAsync() fails and returns an old, undesired version of data, UpdateAsync can check for this with the incremental value. In most cases of saving data, the DataId will match the previous DataId. This means the two sets of data are consecutive, therefore can be saved without worry. However, as @1TheCutestDog mentioned:

You can check for this though with UpdateAsync() as the two DataId values will not match, allowing you to cancel the save and respond accordingly.

9 Likes

Helps to use the whole context and not just a select part of it to make a point.

oldValue is indeed what would be passed from a GetAsync call, however the retrieval and transformation of that value is handled all with a single request from the budget. It’s the value, not the call. DataId is an example in this code, it’s nothing relevant.

On top of the fact that UpdateAsync allows you to specify a transformation function and reject save attempts, you miss the merit when you use SetAsync

  • UpdateAsync is the canonical way to update data. Get should be used to retrieve data and set should be used when you need to force data. This is also an official recommendation as stated on the Developer Hub.

  • UpdateAsync respects previous data in the DataStore. If you don’t use this, you have to tread fairly carefully when doing a Get-Set method.

  • UpdateAsync respects conflicting calls. Data won’t force itself as the new value or try to overstep other calls that are also attempting to write to the DataStore.

  • Good practice. :triumph:

14 Likes

Great tutorial! I’ll definitely be checking how I wrote the DataStore scripts in my old projects.

3 Likes

Thanks for the additional points! I’ve added these to the main post.

1 Like

Your points are mostly valid, however, for saving player data, there is no real reason not to use SetAsync.

  • UpdateAsync is used for keys that you are updating frequently and that multiple servers could write to at the same time, and for this case, you use UpdateAsync, not SetAsync as you could be overwriting or ignoring other values the other servers may have written to it (that are pending).

  • For player data: you save the player’s data only when the player leaves or when an important action is happening. For this case, you use SetAsync or UpdateAsync. Which you choose is up to you. Just know that it doesn’t really matter.

I am not trying to say that UpdateAsync is horrible or that SetAsync is horrible, I was just trying to prove my points related to saving player data.

6 Likes

You should also read the post. It’s not necessarily about preventing data loss because there’s no surefire way to do so when we have zero control over the servers that the data is uploaded to. Even then, data loss is not something that can be permanently prevented but it can be heavily mitigated to give that comforting feel that your servers are fail proof.

UpdateAsync has more usefulness than often thought. No one’s going to hound you for not using it but people are going to pitch that recommendation out for you. In some cases, if you’re working on a project, this may prompt a rewrite depending on the decision of a project head.

Not necessarily. Both methods are equally at risk for data loss or incorrect updates if you have “bad coding structure”. The difference is that SetAsync forces data. It doesn’t respect conflicting calls, it doesn’t transform data, it doesn’t validate data and you can’t cancel it once you initiate it. Obviously you can set up your own code but you can’t get all the behaviours from UpdateAsync in an API incorporating SetAsync (such as overriding calls).

There isn’t much to see in the first place. I could use general examples, avoiding any custom developed APIs.

SetAsync:

local Data = {} -- Data

DataStore:SetAsync("Key", Data) -- Save

UpdateAsync:

local Data = {}
local NO_SAVE = false

DataStore:UpdateAsync("Key", function(oldData)
    if NO_SAVE then
        return nil -- Cancel the save
    end

    if oldData then -- Something exists
        -- Merge contents and return table
    else
        return Data -- If there's no old data, use current set
    end

    return nil -- If no code above this returns anything, cancel save
end)

This point ignores a lot of the merit to UpdateAsync raised in this thread. Reasons have been provided as to why UpdateAsync is powerful and useful. I don’t feel like parroting those points, but this is not the only benefit.

This whole thread and the discussions in the responses should be reason enough? No method is better than another, though one method is more appropriate than another depending on the presented scenario. I’m not sure what point you’re trying to make here.

That depends on what kind of difficulty you’re referencing. It can be difficult to work with DataStore failures in mind. Difficulty using the actual API is a different story of understanding how to apply the service and use it’s methods to achieve your goal.

You do, if you wrap it in a pcall. The only time you don’t is if the call is successful and doesn’t return an intended value (e.g. the incident report wherein DataStores were caching and returning nil instead of actual data).

You know this as well. UpdateAsync is a callback, it returns the data that is entered to the key in the DataStore after the call is completed (whether it saves that data or not). This is their actual data. You can also confirm, with this return, whether it saved or not.

14 Likes

No one is saying don’t use SetAsync, but it is discouraged for using it in a majority of cases. Realistically you should only be using SetAsync if you need to force a certain type of data to stick without respect to any other calls. Player data is a very fragile and significant thing, so mitigating any chance of failure or impropriety is valuable.

Again, that also depends on the scenario at play. For a player leaving, I would change it depending on what is going on during that leave. You can’t differentiate between a teleport and a regular leave; only that the player is gone.

For example, a game I’m developing involves teleporting between places fairly often (but not extremely frequent). I would rather use UpdateAsync so that a player can enter another server with accurate data (and so I can pull off some neat tricks, such as sending the return of UpdateAsync with the player to the new place via MessagingService or server-sided teleport data).

Old relevant discussion:

There may be other threads discussing the usage of these two. I just remember this one off of the top of my head.

6 Likes

My problems with the argument for UpdateAsync() still stand as you have not provided an answer to any of them, merely echoed what was already said in the OP:

  • You say that it will help data be more up-to-date when a player teleports from place to place, but never explain how it does this. Unless it saves the data faster I fail to see how it helps at all.
  • Perhaps I’m being naive, but I don’t see why respecting previous data is at all important, or even possible in certain scenarios (such as mine where I’m saving complex inventories that can change vastly from save to save).
  • Being able to “cancel” the save seems useless when you can just not initiate a save in the first place.
  • If GetAsync() returns an old value, why wouldn’t UpdateAsync() pass an old value?
5 Likes

I feel like there is a really vital point being missed in this thread. If you use UpdateAsync the same way you would use SetAsync, meaning you only set data, almost all the value in using UpdateAsync is lost. UpdateAsync can be super useful, but to maximize its usefulness, your data structures need to revolve around changes and transitions in data. Consider the following cases:

  1. You are using SetAsync twice at the same time, for incrementing how many “points” a player has. Data has now been lost because it was overwritten. Ouch!

  2. You use UpdateAsync twice at the same time for the same reason, but you use something akin to a “data id” as mentioned in this thread. Data has now been lost because an entire set of data was rejected, because it had the wrong id. Ouch!

How do we solve this? By locally storing what has changed, and then using UpdateAsync to commit those changes. When a player gains points, use UpdateAsync, and increment the old value, retrieved from the UpdateAsync callback, by the amount changed. This is what the entire purpose of UpdateAsync is, because it gives you the most recent data in the callback. You can use this on complex tables, all you need is the correct implementation. And if you want optimal data safety, you should.

slight edits for wording

18 Likes

Something I’ve suggested to devs before is to use a “First time playing!” badge or something of the sorts, which is given after you first SetAsync (successfully) that player’s data, then whenever you GetAsync, if they have the badge but no data then there must be an error with the data-stores, so handle that appropriately. This could alleviate data-loss if that is a concern.

5 Likes

I’m quite sure I did. Throughout the post, I’ve mentioned that one of the key differences between Set and Update are how they operate - said that a few times.

GetAsync caches. This means that the data you give the DataStore from SetAsync may not necessarily be the same thing that you get from another GetAsync. On the other hand, as UpdateAsync is a callback, not only does it update the data in the DataStore but it also returns the new value that was saved to the DataStore. Even if GetAsync caches at this point, you can use the return from UpdateAsync instead of spending another request on GetAsync.

A place-to-place teleport is a very specific scenario I brought up as a preference, considering I manipulate data fairly often in said project. Multiple servers and places have the opportunity to manipulate data, therefore I don’t want calls to conflict and save the wrong data for players.

The speed difference between SetAsync and UpdateAsync is heavily negligible and not something worth bringing to the table when discussing this. It’s mainly about the practice of which one to use in what scenario, why and what the merits or caveats of each method are.

It’s not just about respecting previous data, it’s also about respecting conflicting calls. UpdateAsync also calls the transform function as many times as needed and ensures that data saves in the DataStore. SetAsync does not do this - it is a one and done, with nothing to be returned.

While this is a fair point in itself that you can avoid initiating a save, it’s not at all useless. Avoiding using SetAsync if certain conditions do not pass is the same as passing nil in UpdateAsync. That doesn’t make it useless. It’s the same method with a different implementation.

I believe that a cancelled UpdateAsync does not spend any budget. Correct me if I’m wrong about that.

I’m not too sure what you mean by this. Both pass the value from the DataStore. GetAsync can cache the result, UpdateAsync returns a live result to be used with the transform function. When the update is finished its call, it returns what is currently in the DataStore.


This all in mind, your implementation is first and foremost the most important thing between distinguishing which one is best for your use case or if there is any difference at all. I don’t want to parrot anything, though filip’s post above is something to consider.

6 Likes

So would calling UpdateAsync() every time a change is made (e.g. adding a new item, increment their cash) to the player’s data (complex table) be better than doing a one time SetAsync() once the player leaves/auto-saving with SetAsync()?

I would assume yes but I just want to confirm.

1 Like

It depends on your usage. You said you only wanted to save when the player leaves, or automatically every X seconds. SetAsync is completely fine for this. You can also use UpdateAsync.

2 Likes

Not at all. Calling any DataStore methods every time a change is made is bad practice, regardless of which write method you use. What you should be doing is caching data in ModuleScripts (or ValueObjects at the cost of memory, efficiency and more) designed to handle data for the play session.

All data should be held somewhere. When you need to update data, you use a method that the ModuleScript exposes to write to a data table. After that, you should be using DataStore methods minimally for various cases (saving [leaving, autosave, manual save, after purchases], loading [first join, refreshing], writing [deleting data], etc).

This all being said, your second use case - when a player leaves or the game autosaves - is completely valid and fine.

Always try to keep your production in a Lua environment and be mindful of DataStore limitations. Saving every change is bad practice and will undoubtedly lead to throttling.

8 Likes

Ah okay, thanks for the clarification.

Hi, is this tutorial still recommended to use?

And is it better than data store2?

If you’re less familiar with lua I’d recommend using DataStore2 as they handle a lot of the technical work for you, otherwise feel free to utilise this tutorial (which is up-to-date but requires a bit more know-how).

1 Like

I’m quite familliar with Lua, but not as much with Data stores.

My only problem is that the Data Store 2 tutorial is not very in-depth tutorial.

Anyways, i’ll try to learn Data Store 2 a little more.

Thanks for taking time to answer me, big fan of you and your work!

1 Like

I share your disgust with DataStores and appreciate the talk about using them wisely. I agree the hardest thing to work with is DataStores simply because they are not reliable.

1 Like