I've been using assertions on all types of programs for a while now, and they have increased the level of quality and reliability of my software, especially in unsafe languages like C which allows you to do dumb stuff and offers no safety net.
And recently I've been doing a ton of game development in Godot, mainly in C++ with GDExtensions, and I've found assertions to be even more powerful than in systems programming since writing tests in games is not easy.
explode()
Concept
For those who don't know, the concept behind assertions is very simple, you have a condition that should always be true, so you assert that this is the case. It's equivalent to an if statement, where if something is true you explode:
Assert(user != null, "User should never be null");
// equivalent code
if (user == null) abort("User should never be null")Putting them into practice, though, is a bit harder, but before we talk more about their usage, we have to first talk a bit about their history.
History
The first practical implementation of assertions I found was in the 70s when assertions were added to C through the <assert.h> header. And since then, many developers have used them in different ways, the most common way to run assertions is in only debug mode. 
It's better than not having any assertions at all, and in some cases it makes sense, but mostly it doesn't.
A lot of developers have used them in production for reliable software that should usually never crash, for example:
Aerospace and safety-critical systems:
- NASA flight software
- Airbus/Boeing avionics
- Medical device software (FDA-regulated)
- Spacecraft control systems
Operating system kernels (selective critical checks):
- Linux kernel (11,000 uses of BUG_ON for unrecoverable states)
- Fuchsia OS (Google's microkernel)
Database systems:
- TigerBeetle 
- Oracle Database (for critical invariants)
- CockroachDB (for critical invariants)
Among others...The reason why assertions are used in these kinds of reliable systems is the belief that failing fast is safer than continuing with a corrupted state. As well as asserting invariants from the early stages of a system, it prevents a lot of bugs from the very beginning and makes that feature a lot more reliable.
The argument against them
I've gotten a lot of arguments against assertions in production in games, the most common ones are:
- "Everyone hates games that crash."
 - "Bugs are inevitable, so why bother?"
 - "Performance overhead (lol)"
 
So let me go over them one by one.
Everyone hates games that crash
The funny thing about this argument is that the games that don't use assertions on production are usually the ones that crash the most because they usually don't test invalid state.
Imagine a game where an enemy AI holds a reference to the player, but doesn't check if that player entity is still valid:
void Enemy::Update() {
  Vector3 playerPos = target->GetPosition();
  MoveTowards(playerPos);
}What happens without assertions?
Step 1: Player dies, respawns at checkpoint
Step 2: Enemy updates, reads target->GetPosition() from zeroed memory
Step 3: Enemy AI moves toward (0,0,0) or some random spot
Step 5: Enemy collides with terrain in a weird way
Step 6: The Physics system tries to resolve the collision with a corrupted state
Step 7: Physics writes to an invalid memory address
Step 45: Player opens inventory
Step 46: Crash in the inventory rendering code, completely unrelatedThen you receive a bug report from a player saying their game crashed when they opened their inventory. How do you debug this? Now, imagine you have an assertion that checks the player is not a nullptr and is alive:
void Enemy::Update() {
  Assert(target != nullptr && target->IsAlive(), "Enemy %d should never have an invalid target reference", entityId);
  Vector3 playerPos = target->GetPosition();
  MoveTowards(playerPos);
}This not only sets your world view of game state very clearly but also helps you find this bug yourself in development before it even reaches the user. And if it ever does, the user will have a clear error message when the game crashes.
Bugs are inevitable, so why bother?
This one is the most stupid one I've gotten, it's like saying, "Accidents are inevitable, so why bother with seatbelts?"
All games have bugs, that's true, but the point of assertions isn't to eliminate bugs but to control when and where they explode as well as letting the person reading your code know that "Hey x should never happen" as a comment that holds true for the entirety of the program.
Without assertions, bugs follow the path of least resistance. Here, we return null in GetEquippedWeapon due to some edge case, then we access the GetDamage function, which might or might not segfault depending on the amount of padding behind the pointer, and if it does not, then we get a random number:
Weapon* weapon = GetEquippedWeapon(); // returns null due to some edge case
int32_t damage = weapon->GetDamage(); // garbage memory, might segfault might not
// didn't crash, now damage is `2847592`
Player::TakeDamage(2847592);
health -= 2847592;With assertions, we fail as soon as this worldview doesn't hold true, and we give context to the developer or the user instead of a segfault.
Weapon* weapon = GetEquippedWeapon();
Assert(weapon != nullptr, "Weapon should never be null, no equipped weapon in slot %d", currentSlot);
// crashes immediately and with contextOnce again, you probably would catch this through development thanks to asserting invalid state you are notified immediately when your assumptions are incorrect and not at 5 am through an email when everything is crashing and burning, and if you are, it's at least with some nice assertion error message :D
Performance overhead
Another great one, thanks to modern compilers and branch prediction, your code will generally compile to something like this, which basically tells your CPU, "Hey, it's very improbable this ever holds true".
// original code:
Assert(player != nullptr);
// compiles to:
if (UNLIKELY(player == nullptr)) [[unlikely]] {
  TriggerAssertHandler();
}And since our assertions should never fail, with a modern CPU's branch predictor, the overhead is minimal.
However, we need to account for cache behavior, which will be better or worse depending on how you keep your data, this is why the concept of ECS is powerful. Depending on your program, assertions often check variables that may not be in the L1 cache:
Best case (L1 cache hit):  ~1-2 nanoseconds
Common case (L2/L3 hit):   ~10-30 nanoseconds  
Worst case (RAM miss):     ~100-200 nanosecondsWe are assuming an average of ~20 nanoseconds per assertion, if your frame budget is of 5ms, aka 200fps:
5ms frame budget = 5.000.000 nanoseconds (200 fps)
5.000.000 / 20 = 250.000 assertions per frameYou'd need to execute 50.000 assertions per frame to consume 1ms of your entire budget, and this is a conservative case, so I wouldn't worry much about using them extensively.
Usage
Let's talk about the fun part, usage. The most basic and used assertion is the Null assertion, which, of course, asserts that whatever you are given is not null.
void RenderSystem::DrawMesh(Mesh* mesh, Material* material) {
  Assert(mesh != nullptr, "Mesh should never be null");
  Assert(material != nullptr, "Material should never be null");
  
  mesh->Bind();
  material->Apply();
}I usually create my own assertion abstraction on whatever language I'm using (since defaults per language are weird), so here I would use my Assert.NotNull(mesh, "...") function. This assertion is often compared 
to the defensive programming equivalent:
void RenderSystem::DrawMesh(Mesh* mesh, Material* material) {
  if (mesh == nullptr || material == nullptr) {
    return; 
  }
  
  mesh->Bind();
  material->Apply();
}But the main problem with that approach is the silent fail, and sure you could log the error, but what for when you could just assert it?
Then you have the Unreachable assertion, which can be used for exhaustive checks for switch statements, for example:
void Player::UpdateMovement(MovementState state) {
  switch (state) {
    case MovementState::Idle:
      velocity = Vector3::Zero;
      break;
    case MovementState::Walking:
      velocity = forward * walkSpeed;
      break;
    case MovementState::Running:
      velocity = forward * runSpeed;
      break;
    case MovementState::Jumping:
      velocity.y = jumpForce;
      break;
    default:
      Assert(false, "Unreachable: Invalid MovementState %d", (int32_t) state);
      break;
  }
  // ...
}Here we check the MovementState enum, and if it isn't any of the ones we have, we assert it. This one is one of the safest assertions to make since there should never be a point in time when this happens, and if there is, it's most likely a bug.
Again, in my case, I have an Assert.Unreachable("Invalid state ...") function to keep it clean.
One type of assertion I learned recently is the implies assertion. Imagine, for example, you have a load level system, where the player has to go through sequentially by each level, Level 0 -> Level 1 -> ..., then you can assert the previous level was completed, so if the level is higher or equal than 1,
assert that levelNumber - 1 is completed.
void GameState::LoadLevel(int32_t level) {
  Assert.Check(level >= 1 && level <= MAX_LEVELS, "Invalid level number: %d", level);
    
  if (level >= 1) { /* (level >= 1) implies that (prev level is completed) */
    Assert.Check(IsLevelCompleted(level - 1), "Cannot load level %d without completing level %d", level, level - 1);
  }
    
  currentLevel = LoadLevelData(level);
}This goes with one of the rules of Tiger Style "Not just assert what should happen, but also the negative space that you don’t expect", which is extremely powerful, and that's where interesting bugs can show up.
For example, if players ever found a way to skip levels in speedruns and you don't want this to happen, by asserting you set a roadblock to players trying to bug your game. The same thing applies if we want to remove quest items. For example, if the item you want to remove is a quest item, then assert that the quest is completed:
void Inventory::RemoveItem(ItemID id, int32_t quantity) {
  Assert.Check(quantity > 0, "Cannot remove zero or negative items");
    
  int32_t currentQuantity = GetItemQuantity(id);
    
  Assert.Check(currentQuantity >= quantity, "Cannot remove %d of item %d, only have %d", quantity, id, currentQuantity);
    
  if (IsQuestItem(id)) { /* (is quest item) implies that (quest is completed) */
    Assert.Check(IsQuestCompleted(GetQuestForItem(id)), "Item %d should never be removed while quest is active", id);
  }
    
  items[id] -= quantity;
}This is what I meant by "failing fast is safer than continuing with corrupted state", if we removed the quest item by this point, the user wouldn't be able to complete the quest and they probably would have to reset the save if the quest was important, where maybe restarting the game would fix that invalid state or at least the player would be able to report it.
The Out of bounds assertion is very useful in items in your game or data structures you created, like here for getting items at a certain slot and checking that the index is between
the bounds of the program:
void Inventory::GetItemInSlot(int32_t slotIndex) {
  Assert.Check(slotIndex >= 0 && slotIndex < MAX_INVENTORY_SLOTS, "Slot index %d out of bounds [0, %d]", slotIndex, MAX_INVENTORY_SLOTS);
  return items[slotIndex];
}You can also do this in your linked lists or hashmap implementations as well.
Whenever I get worried I'm adding way too many assertions in a function I think about yet another rule of the holy Tiger Style "Assert all function arguments, return values, preconditions, and invariants. On average there should be at least 2 assertions per function." and when I look back this is usually all I'm doing.
Another rule I like in Tiger Style assertions is instead of adding a comment on top of a function:
/* 
  "On occasion, you may use a blatantly true assertion instead of a comment 
   as stronger documentation where the assertion condition is critical and surprising."
  So instead of adding a comment on top like this:
*/
// INFO: Auto-save points are ALWAYS safe - no enemies nearby
void GameState::SaveCheckpoint() {
  // ... 
}In this case saying that "auto save points must always be safe", I add an assertion making sure that stands true 100% of the time:
/* 
  "On occasion, you may use a blatantly true assertion instead of a comment 
   as stronger documentation where the assertion condition is critical and surprising."
  You can do this:
*/
void GameState::SaveCheckpoint() {
  Assert.Check(GetNearbyEnemyCount() == 0, "Checkpoint should never have > 0 enemies, total enemies: %d", GetNearbyEnemyCount());
  // ...
}Even if you have made it "imposible" and it might seem obvious, it might not be for others reading your code or for yourself in the future, and you can keep this statement true through time.
And finally I'm gonna talk about design by contract assertions, which basically states that 
Contracts always involve TWO parties, so we have to assert at both the call site AND the definition site. 
Even when the assertions look identical, they form an "airlock", you can verify each side independently as long as the assertions between them are compatible:
// call site
void CombatSystem::DealDamage(Enemy* enemy, int32_t damage) {
  Assert.Check(damage > 0, "Damage must be positive: %d", damage);
  Assert.Check(enemy->IsAlive(), "Cannot damage dead enemy");
  
  int32_t returned = enemy->TakeDamage(damage);
  
  // postcondition: verify what we got back
  Assert.Check(returned <= damage, "Cannot return more damage than dealt");
}
// definition site
int32_t Enemy::TakeDamage(int32_t damage) {
  // same checks but from the function's perspective
  Assert.Check(damage > 0, "Damage must be positive: %d", damage);
  Assert.Check(IsAlive(), "Cannot damage dead enemy");
  
  int32_t actualDamage = Min(damage, health);
  health -= actualDamage;
  
  // guarantee what we return
  Assert.Check(actualDamage <= damage, "Cannot return more damage than dealt");
  return actualDamage;
}This one is really powerful as well and will help you catch bugs early whenever you have two systems that interact with each other, which as mentioned, makes refactoring systems feel so much safer.
Assertions on existing codebase
Some assertion techniques are harder than others and effectively adding assertions to an existing codebase is pretty hard, specially if its not yours, so there are some things that you have to keep in mind.
1. Only assert properties you are 100% confident about
For production assertions, you should be 100% confident the property will hold. Meaning you have to check all the state that reaches this point and make sure your condition holds true
100% of the time, if you are not sure this is true, then you can leave it as a TODO. On the other hand if you find that your condition does not hold true but it should then get ready 
for some refactoring.
For debug assertions, aim for at least 90% confidence, if assertions don't already exist in the codebase or other developers don't follow the practice then it's better to only add debug assertions.
2. Don't assert the external world
Only assert properties guaranteed by code you control. Don't assert:
- User input (keyboard/mouse/...)
 - Data from network requests (multiplayer games)
 - Config data (unless you're 100% sure about data in the user config files, and are already asserting saving)
 
Use error handling and logging instead. Since of course, you can't guarantee this.
3. Not all assertions cost the same
Even though I talked about assertions not costing much before, depending on what you want to assert you should most likely have an Assert.Expensive(...) which asserts really expensive stuff
like checking a certain set of mobs is alive, or that all the values from somewhere are valid at a certain point in time, etc.
The first two are inspired from A programmer’s field guide to assertions.
Conclusions
By this point you might have realized that apart from handling invalid state, a lot of language limitations or general language bad design can be addressed with assertions, for example:
- Null safety 
- Implications
- Exhaustive type checks or Unreachable state
- Bounds checking
- Range constraints
- Numeric overflow/underflow detection
- Preconditions and postconditionsSo they allow you to use your favorite language and keep your program safe without needing extremely complex type systems (take that Rust).
This is why I believe they are so important to learn and use well, they can take your software to a whole other level of reliability.
Anyways this was all for assertions in Game Dev, I hope you enjoyed reading and consider subscribing to the newsletter if you did :D