openskills.info

Debugging Techniques

itSoftware engineering

Debugging Techniques

Debugging is the work of explaining a difference between expected behavior and observed behavior. The final code change matters, but the explanation comes first.

A strong debugging session turns uncertainty into evidence. You reproduce the failure, narrow its scope, form a hypothesis, and run a test that could disprove it. Each result changes what you investigate next.

This method works across application code, services, devices, networks, and data pipelines. The tools change. The reasoning loop stays recognizable.

Start with a precise failure

Write down three facts before changing code:

  • What behavior did you expect?
  • What behavior did you observe?
  • Under which inputs, environment, version, and timing did it happen?

An error message is evidence, not a diagnosis. Preserve the exact message, stack trace, input, timestamp, and relevant version. A vague report such as “search is broken” creates a large search space. “Search returns an empty page for quoted terms after the July deployment” gives you boundaries to test.

Confirm the failure yourself when possible. A repeatable reproducer gives you a controlled experiment. If the failure is intermittent, record both successful and failed runs. Compare their inputs, timing, state, and environment.

Do not confuse reproduction with root cause. Reproduction only proves that you can trigger the symptom under known conditions.

Build a model before choosing a tool

Map the path from input to output. Identify the components, state transitions, and interfaces involved. Then ask where the first incorrect state could appear.

For a web request, the path might be:

browser → gateway → service → database → service → browser

For a local function, it might be:

arguments → validation → transformation → stored state → return value

Your model does not need every implementation detail. It needs enough structure to place observations. Logs, metrics, traces, tests, and debugger state become useful when you know what each observation says about the path.

Run the evidence loop

Use the same loop until one explanation survives:

  1. Observe the failure and establish a baseline.
  2. State one plausible hypothesis.
  3. Predict what you should observe if that hypothesis is true.
  4. Run the smallest safe test that separates it from alternatives.
  5. Record the result and update your model.

Good hypotheses are specific and falsifiable. “The cache is bad” is weak. “The service reads a stale cache entry because invalidation does not run after this update” predicts observable state and a code path.

Prefer tests that divide the search space. Check a boundary between components. Compare one working input with one failing input. Disable one variable in a safe test environment. Use version history to locate the first bad change.

Change one relevant variable at a time. Several simultaneous changes may remove the symptom without telling you which change mattered.

Choose evidence by question

Different evidence answers different questions:

EvidenceQuestion it helps answer
ReproducerWhich conditions trigger the failure?
TestWhich behavior violates an executable expectation?
LogWhat event and context did the program record?
MetricWhen and where did aggregate behavior change?
TraceWhich path did one request take across components?
BreakpointWhat state exists at one execution point?
WatchpointWhen does a selected value change?
Call stackWhich active calls led to the current point?
Version bisectionWhich change first separates good behavior from bad?

More data is not automatically better. Add an observation when it can confirm or reject a hypothesis. Unfocused logging can bury the signal and expose sensitive data.

Use a debugger with intent

A breakpoint pauses execution at a chosen location. Inspect the current values and call stack there. Step into a call when its internal behavior matters. Step over it when you only need its effect on the current frame.

Conditional breakpoints reduce noise by pausing only when an expression is true. Watchpoints pause when an expression changes. Event and exception breakpoints pause on a relevant event instead of a guessed source line.

A debugger changes execution timing and may alter a timing-sensitive failure. When pausing hides the symptom, favor logs, traces, metrics, or record-and-replay tools that disturb execution less.

Narrow regressions with history

When an older revision works and a newer revision fails, version history becomes a search space. Git bisect repeatedly selects a revision between a known good point and a known bad point. Your test labels each selected revision.

The test must classify behavior reliably. If the test is flaky or the old revision cannot run in the current environment, the result can mislead you. Mark untestable revisions as skipped instead of guessing.

Separate diagnosis, mitigation, and correction

A mitigation reduces immediate impact. A correction removes the identified cause. They may be different actions.

Restarting a process can restore service while destroying useful state. Increasing a timeout can hide a slow dependency without explaining it. During an incident, mitigation may come first. Preserve enough evidence to continue diagnosis afterward.

Before declaring success, explain the causal chain:

trigger → incorrect state transition → visible symptom

Then verify the correction under the original failing conditions. Add a regression test when the behavior can be tested reliably. Check nearby cases and observe the system after release.

Know the limits

No single technique proves a root cause by itself. A correlation suggests where to look. A stack trace shows an execution path. A passing test covers only the behavior that test exercises.

Production debugging also has safety limits. Avoid destructive experiments, unbounded diagnostic logging, and exposure of secrets or personal data. Prefer a safe replica or controlled environment. If you must test production, define the expected effect and a rollback path first.

Some failures resist exact reproduction. Races, resource pressure, hardware faults, and distributed timing can depend on conditions you cannot recreate on demand. In those cases, improve observability and gather evidence across occurrences.

A durable working habit

Keep a short debugging log. Record the timestamp, observation, hypothesis, experiment, and result. This prevents repeated work and makes handoffs clearer.

The core discipline is simple: do not ask only, “What can I change?” Ask, “What evidence would make this explanation wrong?” That question turns random edits into controlled learning.

Relevant careers