Debugging
Summary
Debugging is the systematic process of figuring out why a program isn't doing what it should, and fixing the actual cause rather than the visible symptom. It's a skill separate from writing code in the first place — forming a hypothesis, gathering evidence to test it, and narrowing down a bug's true location instead of guessing and changing things at random.
Mental Model
Think of a bug like a detective case, not a random search. The program produced a wrong outcome; that outcome is the crime scene. Guessing wildly and changing code until something looks different is like arresting random passersby and hoping one confesses — it might work eventually, but it teaches you nothing and often introduces a second bug on top of the first. Real debugging works backward from evidence: what's the exact wrong behavior, what's the last point where things were still correct, and what changed between those two points? Each check either eliminates a suspect or narrows the search — the same binary-search instinct used for a lot of programming problems, applied to "where in this code did things go wrong."
Core Explanation
- Reproduce first. A bug that can't be reliably triggered can't be reliably confirmed as fixed. The first step is finding the smallest, most reliable set of steps that makes the wrong behavior happen every time.
- Breakpoints and stepping. A breakpoint pauses execution at a chosen
line; from there, stepping line-by-line (
Step Over,Step Into,Step Out) while watching variable values is the most direct way to see exactly where an assumption about the program's state stops matching reality. - Logging.
Debug.Log(Unity) orUE_LOG(Unreal) statements placed at key points establish a timeline of what actually happened, which matters especially for bugs that are hard to pause on — timing-dependent logic, physics, multiplayer. - Bisection. For a bug that appeared "at some point" across many changes (or frames, or updates), checking a point roughly halfway between "known good" and "known bad" and repeating cuts the search space in half each time — much faster than checking changes one at a time from the start.
- Isolate the minimal repro. Stripping away everything not required to still see the bug — unrelated systems, unrelated objects in the scene — usually reveals the actual cause directly, because most of what's left once nothing else can be removed is the cause.
Syntax / Examples
C# (Unity) — structured logging
public class EnemyAI : MonoBehaviour
{
void Update()
{
float distance = Vector3.Distance(transform.position, target.position);
Debug.Log($"[EnemyAI] distance={distance:F2} state={currentState}");
if (distance < attackRange)
{
Debug.Log("[EnemyAI] entering Attack state");
currentState = AIState.Attack;
}
}
}
A consistent, filterable prefix ([EnemyAI]) makes it possible to isolate
one system's log output from everything else printing to the same console
during a session.
C++ (Unreal) — log categories
DEFINE_LOG_CATEGORY_STATIC(LogEnemyAI, Log, All);
void AEnemyAI::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
const float Distance = FVector::Dist(GetActorLocation(), Target->GetActorLocation());
UE_LOG(LogEnemyAI, Log, TEXT("distance=%.2f state=%d"), Distance, (int32)CurrentState);
}
A dedicated LogEnemyAI category can be filtered independently in
Unreal's Output Log, the C++ equivalent of the [EnemyAI] prefix
convention above.
Unreal Blueprint
Print String nodes are Blueprint's Debug.Log/UE_LOG equivalent;
Blueprint also supports breakpoints directly on graph nodes (right-click →
Add Breakpoint), pausing execution in the visual graph exactly like an
IDE breakpoint pauses a line of code.
Practical Example
A player's health sometimes drops to a value that doesn't match any known damage source. The systematic approach, rather than guessing:
public class Health : MonoBehaviour
{
private float current;
public void ApplyDamage(float amount, string source)
{
// Log every call site and amount before touching state, so the
// exact sequence of changes is fully visible if something is wrong.
Debug.Log($"[Health] -{amount} from '{source}' (was {current})");
current -= amount;
Debug.Log($"[Health] now {current}");
}
}
Every caller of ApplyDamage is now forced to identify itself via
source, turning "health changed unexpectedly" into a searchable log of
exactly which system, and how much, at every point in time — the mystery
becomes a matter of reading the log rather than guessing at the cause.
Common Mistakes
- Changing code before confirming what's actually wrong. Editing several things at once "to see if it helps" makes it impossible to know which change (if any) actually mattered, and risks introducing a new bug indistinguishable from the first.
- Debugging in a complex scene instead of isolating a minimal repro. Trying to find a physics bug in a full level with 50 active systems is far harder than reproducing it in an empty scene with just the two objects involved.
- Trusting assumptions instead of checking them. "That value can't be null here" is exactly the kind of assumption worth a breakpoint or log line to actually verify, rather than ruling it out from memory — it is frequently wrong in exactly the cases that matter.
- Removing logging/breakpoints without understanding why the bug is fixed. A fix that "made the symptom go away" without a clear explanation of the actual cause often means the underlying bug is still there, waiting to resurface differently.
Best Practices
- Reproduce the bug reliably before attempting a fix — a fix that can't be verified against a repeatable failure can't be confirmed as correct.
- Form a hypothesis before adding a breakpoint or log line: "I expect X to be true here" — a breakpoint without a specific question to answer wastes the opportunity to actually learn something from it.
- Prefer removing/disabling systems to isolate a bug over adding more code around it — the smallest scene/script that still reproduces the issue is almost always the fastest path to the real cause.
- Leave meaningful, prefixed/categorized logging in place for systems known to be tricky (networking, save/load, AI state) rather than stripping every log statement once a bug is fixed — the next bug in the same system will need the same visibility.
Exercises
- Take a script with a deliberately introduced off-by-one bug (see the Arrays concept's Common Mistakes for an example) and practice finding it using only breakpoints and stepping — no reading the code top to bottom first.
- Write a minimal repro scene for a hypothetical "enemy sometimes attacks twice per second instead of once" bug, listing exactly what you would strip out of a full game scene to isolate it.
- Add structured, prefixed logging to a small system of your choice and use it to answer a specific question about the system's behavior (e.g. "how many times does this method run per second?").
- Describe, step by step, how you would apply bisection to find which of 20 recent commits introduced a regression, without checking all 20 individually.
Related Concepts
- Exceptions — reading a thrown exception's message and stack trace is often the fastest starting point for a debugging session, pointing directly at the failing line.
- Functions — understanding a function's inputs, outputs, and side effects is the foundation debugging builds on: most bugs are a function doing something other than what its caller assumed.
References
KNOWLEDGE_SCHEMA.md— GDKB's concept maturity ladder and cross-linking rules, which this document follows.- Unity Manual — Debugging your Code: https://docs.unity3d.com/Manual/ManagedCodeDebugging.html
- Unreal Engine documentation — Debugging Blueprint and C++: https://dev.epicgames.com/documentation/en-us/unreal-engine/debugging-and-error-handling-in-unreal-engine