Common Bug Types
Part of Programming Fundamentals
Knowing the most common categories of programming mistakes lets you find and fix bugs faster than trial-and-error debugging.
Why This Matters
Every program has bugs. The difference between a novice and an experienced programmer is not the absence of bugs but the speed of finding them. An experienced programmer, faced with unexpected behavior, runs through a mental checklist of the most probable causes and reaches the right diagnosis in minutes. A novice spends hours checking unlikely hypotheses because they lack a map of the failure modes.
For rebuilders programming new hardware with minimal tools, debugging is especially important. You often have no interactive debugger, no error messages, no call stack trace. You have a program that does something wrong, raw memory, and the ability to print bytes to a serial console. Knowing the common bug categories directs your attention to the right location in the code.
Bugs are not random. They cluster around specific patterns that have appeared since the first computers were programmed. Learning those patterns is a force multiplier.
Off-by-One Errors
The most common bug in array and loop code. An off-by-one error means your index or counter is one too high or one too low. The classic forms:
Wrong loop bound: FOR I = 1 TO N when you should use FOR I = 0 TO N-1, or iterating one step too many.
Array out-of-bounds: Accessing A(N) when the array is declared DIM A(N-1) — index N is one past the end.
Fence-post error: When counting N items separated by something, there are N-1 separators, not N. A fence with 10 posts has 9 sections. A string of N characters has N-1 gaps between characters.
Loop body executing wrong number of times: A DO-WHILE loop always executes at least once; a WHILE-DO loop may execute zero times. Using the wrong form causes the body to run one extra or one fewer time than expected.
Prevention: draw a diagram of the first and last iteration. Write out what values the index takes and what the code does at each boundary. Check: does the loop body execute for the first element? For the last element? Does it execute once too many?
When you see corrupted data just past the end of a buffer, suspect off-by-one. When a loop always processes one item incorrectly, suspect off-by-one.
Uninitialized Variables
A variable that has never been assigned a value contains whatever happened to be in that memory location — often zero, sometimes not. Reading an uninitialized variable produces unpredictable results that vary between runs, between machines, and after code changes (because code changes can move variables to different memory locations with different initial contents).
This bug is insidious because the program may work correctly most of the time. The memory location might usually contain zero by accident. But under specific conditions — different startup sequence, different memory layout, different run history — it contains a nonzero value, and the program fails.
Prevention: initialize every variable before first use. In assembly, zero the entire RAM region at startup. In higher-level languages, explicitly set initial values rather than relying on implicit initialization.
When a program behaves differently between two runs with identical input, suspect uninitialized data. When behavior depends on what the program did in a previous session, suspect data left in memory from a prior run.
Integer Overflow
Every integer type has a maximum value. When an arithmetic operation produces a result larger than that maximum, overflow occurs. The result wraps around to a small or negative value.
For an 8-bit unsigned integer, 255 + 1 = 0 (wraps to zero). For a 16-bit signed integer, 32767 + 1 = -32768 (wraps to the most negative value). For a 16-bit unsigned integer holding a byte count, if you calculate SIZE * ELEMENT_SIZE and the product exceeds 65535, the result is wrong.
Overflow often produces results that are dramatically wrong — negative where only positive is expected, or a tiny value where a large one is expected. This makes it somewhat easier to spot than subtler bugs.
Prevention: think about the range of values each variable can hold. Before multiplying, ask whether the product can exceed the type’s capacity. Widen the type to 16 or 32 bits when the range demands it.
The CPU’s carry flag and overflow flag exist to detect these conditions. In assembly language, check them after arithmetic operations when overflow is a concern.
Logic Errors in Conditions
Conditional expressions are written wrong in ways that are syntactically valid but semantically incorrect. Common forms:
Wrong operator: Using > instead of >=, or = instead of <> (not equal). Off-by-one at the logical level.
Wrong logical combination: Writing A AND B when you mean A OR B. This is especially common in bounds checking: “valid if value is between 0 and 100” should be X >= 0 AND X <= 100, but is sometimes written with OR.
Negation errors: The complement of A > B is A <= B, not A < B. De Morgan’s laws: NOT (A AND B) = (NOT A) OR (NOT B). Errors in negation produce conditions that are almost right but wrong at the boundaries.
Assignment instead of comparison: In some languages, IF X = 5 THEN assigns 5 to X rather than comparing. Know your language’s rules.
Prevention: test boundary conditions explicitly. If a condition should be true for values 0-10, test with values -1, 0, 5, 10, and 11 and verify the result. Draw a number line if the logic involves ranges.
Pointer and Address Errors
In assembly language and low-level C, you work directly with memory addresses. Errors here can corrupt any part of memory.
Wrong address: Computing the address of a variable incorrectly — wrong base, wrong offset, wrong scale factor.
Stale pointer: A pointer that was valid but now points to memory that has been reused for something else. Reading from it gives wrong data; writing to it corrupts the new occupant.
Null/zero address dereference: Attempting to read or write through a pointer whose value is zero (or whatever sentinel represents “no valid address”). This reads or writes address zero, which may contain critical system data.
Buffer overrun: Writing past the end of a buffer overwrites adjacent memory. This corrupts variables or return addresses depending on memory layout. Buffer overruns are among the most dangerous bugs because their effects appear far from the cause.
Prevention: draw memory layout diagrams. When writing to a buffer, always verify the length before writing. Check address computations by substituting the first and last valid index and verifying the result falls within expected bounds.
Timing and State Errors
These bugs appear in programs that respond to hardware events, manage state machines, or interleave multiple activities.
Missing state reset: A state machine variable that should be reset to its initial state at the start of a new operation but is left with its value from the previous operation.
Race condition: Two activities (perhaps main code and an interrupt handler) share data, and the result depends on which one runs first. The interrupt fires partway through the main code’s multi-step update, reads a partially updated value, and produces wrong results.
Interrupt reentrancy: An interrupt handler that is not safe to interrupt itself. If the handler does not disable interrupts and the same interrupt fires again before the handler finishes, data structures can be corrupted.
Prevention: in interrupt-driven code, always consider what the main loop’s state might be when the interrupt fires. Use atomic operations (single-instruction reads and writes) for shared state wherever possible. Disable interrupts during critical sections that update shared data.
Finding Bugs Systematically
When a program misbehaves, follow this process before touching the code:
-
Characterize the symptom precisely. “It crashes” is not a bug report. “It crashes when I enter a value greater than 127 for the temperature” is actionable.
-
Form a hypothesis about the cause. Given the symptom and the category of likely bugs, what is the most probable explanation?
-
Design a test that distinguishes your hypothesis from alternatives. If you think it is overflow, try a value just below and just above the suspected threshold.
-
Examine the relevant code with fresh eyes. Often the bug is visible once you know where to look.
-
Fix the specific problem, not the symptom. If adding a bounds check makes the crash go away, that does not mean there is no overflow — find the overflow and fix it.
-
Verify that the fix works for the failing case and does not break other cases.
Systematic debugging is faster than random changes. Every change that does not fix the bug but masks a symptom makes the underlying problem harder to find.