Let me get philosophy out of the way. If you think that the goal of game development is to deliver an experience to the players. Then that's it. Remove or hide anything that would break that experience.
Instead, think of giving a good experience to developers, designers, testers and players.
Now, I can be a bit more pragmatic. DMGregory answer is even more pragmatic.
I present to you the concept of environments:
- Development environment
- Testing environment
- Release environment
These might map to phases, or they might be custom builds.
On your development environment, you want to fail fast. The sooner the developer is aware of a mistake, the faster you can fix it, and the cheaper it will be. The cost of fixing bugs grows with time.
On the testing environment, the focus should be on getting the most information. If you can get stack traces, screen shots, a list of recent input, and so on, then do that. That would make it easier to track down those bug that did not happen often enough.
On the release environment, you still want to get as much information as possible. But you should not inconvenience the player. Furthermore, privacy and law comes into play when collecting information. That is something you should address in an EULA.
Thus, I'm saying that the way you deal with an exception, should depend on the environment.
You should also consider the type of exception.
We have exception from which you can't or it makes no sense to recover (such as a
Then there are exception that you should prevent instead of handle. These are exceptions for developers and should not reach Release (such as
NullReferenceException). It could be that the developer missed them and they got into a testing environment. Code review, static analysts, and tests will help you find them.
On a test environment, you want to gather as much information of how they happened. So that the developer can figure out how to prevent them.
And finally there are exception that you should not prevent, but handle. These are exception about I/O (such as
FileNotFoundException). For these, get a log, and try to recover and keep the game running. Note that even if you check if the file is there, the OS could preempt the thread after the check and when you use it. That gives a chance to another process to delete it.
Exceptions ere, of course, exceptions. Some systems will hide them, leading to a different exception down the road, one that should not be. It is exceptional. For example, the system loading a corrupted file should throw. Yet, they could have made it return null instead. That will lead to a hard to debug
NullReferenceException, which could make its way to Release.
Oh, and null in Unity is a special case, because it isn't actually null.
This reduces the question to: how to handle an exception that should not reach Release, but did anyway? Well, I have one more concept for you…
Defensive programming and Offensive programming. Defensive programming is about checks and validations to prevent errors. While offensive programming is about letting errors happen and either crashing or recovering.
In Release. If you are dealing with external systems, be Offensive (e.g. do not try to prevent I/O exceptions, recover from them). If you are dealing with internal systems, be Defensive.
For example, a saved game is something you did not release with your game, and thus treat as external. Thus, when loading it, the guideline is to use offensive programming. No checks. On a corrupted save file, the system should throw. If it loads well enough to run, and if there is something missing because it failed to load, let it crash.
But a stage is something you did release with your game, and thus you may treat as internal. Thus, when loading it, the guideline is to be defensive. Check that it loads, if it didn't, have a fallback, or disable the content. For example, dialog fails to load, have some default dialog. Nobody should notce.
Note: You do not want the fallback content in the testing environment. Instead, you want testers to catch the problem giving you a chance to fix it.