
As programmers, we can embed fail-safes into our code. In essence, we get decide what failure looks like. As we are writing code we choose how rigorously we validate our work; for example, checking that values are not null, lists are not empty, or numbers stay within valid ranges. In the event that something bad happens, and we correctly wrote code to test for this event, we can reject or accept what happened and let the program continue (failing silently) or completely halt the program in a crash. Failing silently versus crashing is not a difference of programming philosophy – a good programmer will use either strategy depending on the situation.
Failing silently naively sounds like the more appealing implementation because projects will appear to be more robust. However, letting the program continue after an error was encountered can let problems fester and makes it much harder to track down and identify the origin of defects. While crashing doesn’t obscure problems, the negative consequence is obvious – once a project ships crashing is unacceptable. Additionally, during development crashing can lead to some pretty embarrassing demos as well as create blockers for other developers and quality assurance.
Here are some example scenarios.
Failing silently and crashing are basic examples of error handling, how the program responds once an error has occurred. Ideally we want to catch or prevent errors as early as possible; before it affects our co-workers, and definitely before they reach end users.
The reality of game development is that everyone wants a bug-free product but it is hard to convince anyone to invest time in work that is not new features. Time is an extremely limited commodity. Game development is also very organic. We evaluate the project and implement new ideas all the time. So during development we are constantly wiring things together in new and unplanned ways. Efficiently preventing defects in the first place is essential and there have been been many strategies devised to do so:
No single strategy is perfect and they all take time and require updates throughout development. My personal preference on preventing errors is known as Defensive Programming. It is a combination of a couple ideas:
For example, one rule is to always write an “else” case in a conditional statement, or write a “default” case in a switch statement; even if they only contain a warning indicating that you should never hit that case. It takes a little longer to write code with extra sanity checks, but it is the most efficient practice compared to other defect prevention. Let’s look at a pseudo-code example. Consider that we are writing a system where people can make friends. Here is an example implementation that can crash:
class Person { protected _name: string = null; protected _friends: Array<Person>; constructor (name: string) { this._name = name; this._friends = new Array<Person>(); } public get name(): string { return this._name; } public addFriend(friend: Person) { this._friends.push(friend); } public listFriends(): string { console.log(this._name + “ has friends: “); var i: number; var max: number = this._friends.length; for (i = 0; i < max; i++) { console.log(this._friends[i].name); } } }
There is nothing in this first implementation that prevents an instance of Person from having a null value for their name, and nothing preventing “addFriend” from adding a null value to the friends list. If there is a null value in the friends list a crash will occur when trying to access the “name” property in the “listFriends” method. Note that this crash is occurring in a different method than the actual source of the problem.
We can rewrite the “addFriend” method so that it fails silently:
public addFriend(friend: Person) { if (friend != null) { this._friends.push(friend); } }
This fixes the Person class so it will not crash; however, now if a mistake is made attempting to add a null friend it will go uncaught. If I were to rewrite the Person class defensively it would look like this:
class Person { protected _name: string = null; protected _friends: Array<Person>; constructor (name: string) { this._name = name; if (this._name == null || this._name == “”) { console.warn(“Person was created without a name”); } this._friends = new Array<Person>(); } public get name(): string { return this._name; } public addFriend(friend: Person) { if (friend != null) { this._friends.push(friend); } else { console.warn(“Person.addFriend: attempted to add null friend to ” + this._name); } } public listFriends(): string { console.log(this._name + “ has friends: “); var i: number; var max: number = this._friends.length; for (i = 0; i < max; i++) { console.log(this._friends[i].name); } } }
After supplementing the fail silently approach with warnings, our program won’t crash, but flaws in the system are still exposed.
It might seem nuanced or trivial, but tracking down defects is time intensive, particularly on a big project with several programmers, hundreds of files, and tens of thousands of lines of code. Even on a project with a single programmer you could easily have to revisit code that you haven’t touched in over a month. The cost to fix a defect increases dramatically the longer it goes unnoticed, assuming that you find it. Preventing defects means more time and energy are spent on improving the experience than on technical debt.
Best practices for preventing motion sickness while maximizing learning outcomes.
Best practices for preventing motion sickness while maximizing learning outcomes.