Stop using SetAsync() to save player data

#1

I’m as passionate about this as Colbert is about wait() loops:
Stop using SetAsync() when saving vital information such as player data


Background

For the majority of games, there comes a point where data about an individual needs to be saved (e.g. level, items, cash, etc). To achieve this, we use DataStores.

Now ask any developer, what did you find most challenging about creating a game, and you’ll find a good majority respond “DataStores”. There are various reasons for this: they require a good understanding of lua, take significant time to initially learn about and setup, and are difficult to test compared to typical features. There is one issue however which dominates all; one with real-world consequences and potentially disastrous results: the loss of player data.


Consequences of data-loss

The loss of player data can have severe repercussions, both for the user and developer:

  • Significant time spent trying to restore data and refund players.
  • Loss of players/reputation. This is fairly self explanatory; if a player loses their data there’s a good chance of them quitting for good and leaving a negative review in the process.
  • Ethical consequences. To some, a player might just be text and a character on the screen, however you have to realise each individual is a real human. That 1000 R$ might mean nothing to you, but for the player was 10-weeks of saving up pocket money to afford it. Some players may have also invested huge amounts of time into your game. The loss of many-months of progress may cause them personal distress.

What can be done to limit this?

Obviously no developer desires this, however speaking with others this is something almost everyone, including myself, experience at one point or another. No need to fret though, there are measures you can take to prevent or at least minimise data-loss and its effects:

  • Use a separate datastore to specifically handle developer product purchases. If worst comes to worst, this will make refunding players enormously quicker and easier to do. It doesn’t take long to implement and could really pay-off in the long run.
  • Consider setting up backup datastores such as DataStore2. In simple, DataStore2 creates a backup datastore to run alongside your main which can be used to retrieve lost data when necessary. You can find out more here.
  • Verifying data before writing it to the datastore. The data retrieved using GetAsync() is not always accurate or correct. For example, if GetAsync() fails then the players data may be set to false or nil. If this value is written to the datastore, the player’s data will be completely overwritten. GetAsync() can also retrieve incorrect previous data if a player hops between servers too quickly. To prevent this, we use UpdateAsync().

Writing data

There are four methods of writing data:

  1. SetAsync
  2. UpdateAsync
  3. IncrementAsync
  4. RemoveAsync

Now there’s a good chance you’ve heard of SetAsync(), but what about UpdateAsync()? If you haven’t, shame on you as the main page even highlights it in yellow for you:

As the warning explains, SetAsync simply writes a new value to a key without any consideration for the previous value, whereas UpdateAsync retrieves the value of a key from the datastore and updates it with a new value - this gives you the ability to compare the data about to be saved with the previous and act accordingly.


Writing data correctly

So obviously we should all be using UpdateAsync() to save player data, right? Yes! However there is still a handful of people using SetAsync() when they really shouldn’t. Even an official tutorial by Roblox on Saving Player Data uses this incorrectly.

Here’s an example of UpdateAsync() and incremental-values used to effectively save data:

You vs the guy she says not to worry about

Source code
if playersData then
	datastore:UpdateAsync(key, function(oldValue)
		local previousData = oldValue or {DataId = 0}
		if playersData.DataId == previousData.DataId then
			playersData.DataId = playersData.DataId + 1
			return playersData
		else
			return nil
		end
	end)
end

Note: nil is returned for scenarios where we don’t want the data to be updated. It effectively cancels the save. You should also wrap your functions in pcalls when retrieving and setting data.


Epilogue

I’m guilty myself of using SetAsync(). Back in 2017 I had a game which blew up to front page. This was an awesome experience but also created a datastore nightmare at the time too. We were having reports of data-loss at least 20 times a day which, as you can imagine, created a huge headache; the next week of my time was completely devoted to rewriting the datastore and refunding as many players as possible.

In-a-way, this was a blessing in disguise as it forced me to develop good practices when saving data. Now-a-days, I will always use UpdateAsync() with an incremental-value when saving important data. More recently, I released a multi-place game where players are constantly teleporting between servers, yet to this day have had 0 reports of data-loss.


Summary

Here’s a brief overview by @colbert2677:

  • 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

Thanks for reading. Please get in touch if you have any question.

If you’re interested in setting up your own datastore, feel free to check out my other brief tutorial here: How would you use ROBLOX's datastorage API?

51 Likes
Data Saving Issues
Datastore retrieving a nil value?
How could I improve the neatness and efficiency of my DataStore code?
What better ways are there to storing user data in a DataStore?
closed #2

This topic was automatically closed after 1 minute. New replies are no longer allowed.

opened #3
#4

Thanks for the tutorial! Looks like I need to go back and rewrite some code.

1 Like
#5

A really helpful tutorial. I will use this in a future for my big games :slight_smile: :wink:

1 Like
#6

Well, you are talking bad about SetAsync a bit too much, when it comes to complex data management UpdateAsync isn’t suitable for it however I would agree that people should use UpdateAsync over SetAsync in a scenario that your Code is simple and not complex.

My case however I write my own UpdateAsync combined with the same functionality and concept from DataStore2 (Originally berezaa’s method of saving data), Using DataStore and OrderedDataStore to save Data with a Time Line Record.

You also forgot to mention that DataStore has it’s limits, using pcall and some other functionality but I do really like your Threads they are fun to read, give insight and helpful. :+1:

#7

Awesome tutorial, I will definitely use this in the future! Thanks.

2 Likes
#8

Sure, if you’re working on a small project or using non-essential data that’s fine - UpdateAsync does take more time and consideration to implement. However, if you are working with data where players are purchasing goods (e.g. Cash) for real-life currency then it is vital you are doing your best to ensure their data is secure as it has real-life consequences.

Could you provide an example of this? I’m using UpdateAsync() for fairly complex systems in HD Admin which sometimes works with 20k+ concurrent players at the weekends, yet haven’t experienced any technical problems or limitations.

I haven’t included it in this tutorial as I wanted to focus more on the writing aspects than error handling. You are correct though, pcalls and error handling are vital components when managing datastores!

4 Likes
#9

Couldn’t agree more pal!


I made my own Custom DataStore2 with the same functionality however you can’t achieve the same results using UpdateAsync (You could but it might be a longer process)

Although I’m not saying that it isn’t useful because I am using UpdateAsync for Checking a Game Place Version, if there is a new version auto Soft-Shutdown, I might create a system that doesn’t require rejoining but rather update the game internally by itself.

4 Likes
#10

ABSOLUTELY. Instant thumbs up the moment I saw the thread’s title. UpdateAsync is overwhelmingly underused and it shouldn’t be. I don’t know why I never wrote about it but I probably wouldn’t be able to explain it the same way.

There should be very few circumstances in which you actually need to use SetAsync. All other data should be saved with UpdateAsync. The methods are named the way they are for a reason - update respects previous data and conflicting write requests, set does not. That being said, I also now include transform functions for ANY library I create that has to do with handling data as well.

Foregoing the use of UpdateAsync to use SetAsync in all cases of saving is a fairly dangerous practice that needs to be let go by many developers. This is hurtful to both user experience once the problems start rolling in and to the developers for issues related to their projects, as well as to the knowledge they hold of the DataStore API. This could also carry over to other developers, such as when answering questions on the DevForum.

It also cannot be expressed enough that DataStore calls should be wrapped in a pcall! This is so utterly important in terms of handling errors in DataStores yourself and ensuring that issues with DataStore methods do not cause further damage. Anything that is internally (even externally) a web call should be wrapped in a pcall. Write the responses to the console for your own viewing and do something for the user to notify them that their data failed to load. I personally block save requests or merge session data to saved data in case of error.

Cheers for writing this. :tada:

5 Likes
#11

I thought I had to make changes in my code, but apparently my friend already made them for me. (SaveAsync is UpdateAsync, I just haven’t updated it).

All updated!

2 Likes
#12

Oh boy time to change all my datastore code. Thanks for the information though!

1 Like
#13

All current data store code I’ve made previously have been adapted to UpdateAsync().

Thanks for the information, have a nice day.

1 Like
#14

I fail to see how using UpdateAsync() (at least the code you provided) prevents data loss. Wouldn’t oldValue be the exact same thing returned by a GetAsync() call? Or am I missing something here? I also fail to see what the existence of DataId does.

(Assume a 100% success rate with GetAsync() because it is very easy to not save if the call fails.)

6 Likes
#15

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.

5 Likes
#17

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.

2 Likes
#18

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:

5 Likes
#19

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

2 Likes
#20

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

#21

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.

1 Like