VMware ESXi and vSphere Cluster Management

Debugging Fundamentals

Learn a systematic debugging workflow for finding, diagnosing, fixing, and preventing software defects using evidence, tools, and regression tests.

Debugging is the disciplined process of finding, understanding, and correcting defects in software. A bug is a defect or unexpected behavior, such as a crash, incorrect result, slow operation, or failure that occurs only in a particular environment.

This lesson assumes familiarity with variables, functions, conditions, loops, program execution, and basic editor or command-line output. For a concise reference, see Debug.

What Debugging Is

Debugging begins with an observed symptom and uses evidence to determine the root cause: the underlying condition that produces the failure. The goal is not merely to make one visible error disappear. A good fix explains why the problem occurred, corrects the defect, and reduces the chance that it will return.

Reproducibility is especially important. If you can repeatedly trigger a problem with the same inputs and conditions, you can compare program state before and after a change. When a problem is intermittent, timestamps, request identifiers, logs, environment details, and carefully collected inputs provide evidence that may reveal a pattern.

Debugging Compared with Related Activities

ActivityPurposeHow it differs from debugging
TestingChecks whether software behaves as expected under selected conditions.A failing test may reveal a bug; debugging investigates and corrects the cause.
MonitoringObserves system health and behavior in operation.Monitoring can detect symptoms, while debugging explains them.
LoggingRecords diagnostic events and system state.Logs provide evidence used during debugging but are not the investigation itself.
Code reviewExamines source changes for correctness, clarity, and risk.Review can prevent defects; debugging handles a defect or unexpected behavior that has been observed.

Types of Software Problems

Problem categoryTypical symptomUseful evidenceRecommended first step
Syntax or compile-time errorThe program cannot be parsed, compiled, or built.Compiler message, file, line, and diagnostic code.Read the first relevant diagnostic and inspect the indicated syntax or type.
Runtime exception or crashThe program stops while running.Exception type, message, stack trace, inputs, and state.Inspect the failing stack-trace frame and trace the invalid value backward.
Logic errorThe program runs but produces an incorrect result.Expected result, actual result, intermediate values, and test input.Find the first point where actual state differs from expected state.
Performance or resource problemThe program is slow, uses excessive memory, or exhausts a resource.Timings, profiles, memory use, query counts, and workload details.Measure the suspected operation instead of guessing from source code alone.
Configuration or environment failureWorks locally but fails elsewhere.Configuration, environment variables, permissions, dependency versions, and service status.Compare the working and failing environments systematically.
Dependency or deployment failureA package, service, build artifact, or deployed version is missing or incompatible.Build output, dependency lock data, deployment details, and version information.Confirm the exact artifact and dependency versions used by the failing system.

A Systematic Debugging Workflow

StageGoalQuestions to askExpected output
Observe and recordDescribe the symptom precisely.What happened, where, when, and with which input?A factual problem statement.
ReproduceTrigger the issue reliably.Can the same steps produce the same result?Clear reproduction steps.
MinimizeRemove irrelevant complexity.What is the smallest case that still fails?A minimal reproducible example.
Gather evidenceCapture facts about the failure.What do messages, logs, inputs, variables, and system state show?Relevant diagnostic data.
Form a hypothesisPropose a testable explanation.What condition could produce this symptom?A specific prediction.
Test the hypothesisChange or measure one relevant factor.What result would support or disprove the explanation?Evidence for or against the hypothesis.
Find the root causeExplain the failure, not just its location.Where was the invalid state introduced, and why was it allowed?A causal explanation.
Apply a targeted fixCorrect the defect with minimal unrelated change.Does the change address the identified cause?A focused code or configuration change.
Verify and prevent regressionConfirm the fix and protect it.Does the original case pass? What about edge cases?Passing checks and an automated regression test.
DocumentMake the result useful to future maintainers.What failed, why, how was it fixed, and what must remain true?A concise record or issue update.

1. Observe and Reproduce

Record the exact input, expected behavior, actual behavior, time, environment, and reproduction steps. Avoid vague descriptions such as “it does not work.” Prefer a statement such as “A request containing an empty identifier returns an internal error, while a non-empty identifier succeeds.”

When the problem cannot be reproduced, do not immediately guess. Collect more evidence: relevant logs, timestamps, request identifiers, dependency versions, configuration, permissions, and input characteristics.

2. Minimize the Failure

A minimal reproducible example is the smallest self-contained case that still demonstrates the issue. Remove unrelated files, inputs, services, and code paths one at a time. A smaller case makes cause and effect easier to see and makes experiments faster.

3. Gather Evidence and Form Hypotheses

Use error messages, exception types, error codes, stack traces, logs, input data, variable values, and system state. Turn observations into testable hypotheses. For example: “The parser fails because a delimiter is accepted in the successful format but missing from this input.” Test that explanation with a controlled input rather than changing several parts of the program at once.

4. Fix the Root Cause

The immediate failure location is not always the underlying cause. A null-value exception may occur when a field is read, while the real defect is that an earlier function failed to initialize or validate that field. Trace the invalid state back to where it was introduced, then correct the contract, validation, initialization, or error handling there.

5. Verify and Document

Retest the original failing case, related cases, boundary values, and normal cases. Add an automated regression test: a test that ensures a previously fixed issue does not return. Remove or reduce temporary diagnostics, especially diagnostics that expose sensitive data. Record operational or configuration changes needed for the fix.

Reading Diagnostic Output

Error Messages and Exception Types

An error message describes what the program or tool noticed. An exception type often classifies the failure, such as an invalid argument, missing value, permission failure, or unavailable resource. Read the message together with its input and context; the wording may identify the immediate failure without explaining why the invalid state existed.

Stack Traces and Call Stacks

A stack trace is a record of active function or method calls at the time of an error. The call stack is the chain of calls that led to the current execution point. Start with the exception type and message, then find the frame in your own code where the failure was raised or the invalid operation occurred. Read upward through the calling frames to understand how execution reached that point.

Error: missing customer identifier
    at loadCustomer (customer-service.example:42)
    at handleRequest (request-handler.example:18)
    at dispatch (server.example:7)

In this example, line 42 in loadCustomer is the immediate failure location. The caller at line 18 may reveal why the identifier was absent. Frames belonging only to libraries or the runtime can provide context, but application frames usually offer the most direct clues.

Debugger Tools and Techniques

An interactive debugger runs a program under controlled execution. Set a breakpoint to create an intentional pause point, then inspect the program before it continues.

ControlWhat it doesWhen to use it
Set or remove breakpointPauses or stops pausing at a selected line or location.Stop near the suspected code path.
ContinueRuns until the next breakpoint or failure.Skip code that is not relevant to the current question.
Step overExecutes the current line without entering a called function.Follow the current function at a high level.
Step intoEnters the function called by the current line.Inspect a function whose behavior may be wrong.
Step outFinishes the current function and returns to its caller.Leave a function after confirming it is not the source of the issue.
RestartStarts the debug session again.Repeat an experiment from a clean initial state.
Inspect variablesShows local, parameter, and object values.Compare actual state with expected state.
Call stackShows the active chain of function calls.Understand how execution reached the pause.
Watch expressionMonitors an expression while execution is paused.Track a calculated value or condition across steps.
Conditional breakpointPauses only when a specified condition is true.Stop on one record, iteration, or unusual state among many.

Typical language-agnostic debugger actions are:

1. Set a breakpoint near the suspected operation.
2. Run the program under the debugger.
3. Inspect local variables and the call stack.
4. Step over ordinary lines or step into a relevant function.
5. Evaluate expressions and watch important values.
6. Continue until the failure or another breakpoint.
7. Restart and repeat after changing one relevant factor.

Logging and Temporary Diagnostics

A log is a recorded diagnostic event or message produced by a system. Use suitable severity levels such as debug, info, warning, and error. Include timestamps, component names, request identifiers, operation names, and relevant non-sensitive values.

For intermittent failures, structured logs can connect events across a request or job. Do not log passwords, credentials, tokens, personal data, or other secrets. Avoid excessive logging that creates noise, performance cost, or storage problems. Remove temporary messages or lower their severity when the investigation ends.

Assertions

An assertion is a programmatic check that an expected condition holds. Assertions make assumptions visible:

assert accountId is not empty
assert total >= 0

Use assertions for conditions that should be impossible when program invariants are respected. Validate ordinary external input with normal error handling as well, because users and external systems can legitimately provide invalid data.

Isolation Techniques

Change One Variable at a Time

Change only one relevant factor before rerunning the case. If you alter the input, configuration, dependency version, and code together, a successful result does not reveal which change mattered. Keep a short experiment record containing the hypothesis, change, result, and conclusion.

Use Bisection

Bisection, or binary search, repeatedly divides a range to locate the first failing condition or change. For a defect introduced somewhere in a sequence of version-control revisions, test a revision near the middle. If it fails, search the earlier half; if it passes, search the later half. Repeat until the earliest failing change is identified.

The same idea can isolate a failing loop range, input collection, configuration list, or processing pipeline. Each experiment should produce a clear pass or fail result.

Compare Working and Failing Cases

Use a small successful input beside the smallest failing input. Compare characters, formats, lengths, types, ordering, permissions, and environmental assumptions. For incorrect calculations, compare intermediate values and locate the first divergence rather than comparing only the final output.

Stub, Mock, or Isolate Dependencies

A stub supplies controlled responses from a dependency. A mock can also verify that expected interactions occurred. Isolating a database, network service, clock, file system, or third-party API helps determine whether the defect is in your code or in an external condition. After isolation, verify the behavior against the real dependency when integration behavior matters.

Practical Debugging Examples

Runtime Exception from a Missing Value

  1. Read the exception type and message.
  2. Start at the relevant frame in the stack trace.
  3. Inspect the value that was unexpectedly missing.
  4. Trace backward to where the value was introduced or should have been initialized.
  5. Correct initialization or validation and define appropriate behavior for missing input.
  6. Add a test for both the missing-value case and valid input.

Incorrect Calculation

Suppose a loop returns a total that is one item too small. Define a small known input and expected total. Place a breakpoint inside the loop, inspect the index and running total, and step through each iteration. The first iteration where the running total diverges may reveal an off-by-one boundary, an incorrect comparison, or an item skipped by a conditional.

Failure for One Input

Create the smallest input that still fails, then compare it with a successful input. Check the differing character, delimiter, encoding, whitespace, length, and format rule. Once the distinguishing condition is known, add a focused parser test that preserves the failure.

Intermittent Issue

Add structured diagnostic events containing timestamps, a request or operation identifier, component names, relevant state, and non-sensitive input characteristics. Compare successful and failing traces. Look for timing, concurrency, load, retry, ordering, resource, or environment patterns. Avoid relying on a local run if the failure depends on production conditions.

Defect Introduced by a Recent Change

Compare a working revision with a failing revision. Review the changes and, when the introduction point is unknown, use version-control bisection. Test each selected revision with the same automated check until the earliest failing change is found.

Troubleshooting Paths

The Program Crashes with an Exception

  1. Read the exception type and message.
  2. Start from the relevant stack-trace frame.
  3. Inspect inputs and program state at the failure.
  4. Trace the invalid state back to its source.
  5. Add a test for the corrected behavior.

Output Is Wrong but No Error Is Reported

  1. Define the expected result using a small known input.
  2. Pause at key stages or add focused diagnostics.
  3. Compare actual intermediate values with expected values.
  4. Locate the first point where values diverge.

The Issue Appears Only in One Environment

  1. Compare configuration, dependency versions, environment variables, permissions, data, and service availability.
  2. Do not assume local behavior matches the failing environment.
  3. Reproduce with a controlled environment where possible.

The Fix Works Once but the Defect Returns

  1. Confirm that the root cause, rather than only the symptom, was addressed.
  2. Add an automated regression test.
  3. Review related paths and edge cases.
  4. Record any operational or configuration change required for the fix.

Verification and Prevention

After applying a fix, retest the exact original failure. Then test adjacent behavior, empty and boundary inputs, invalid inputs, normal cases, and relevant integration paths. A fix that passes only one example may still fail in a nearby case.

Use version control and small changes during investigation. Small commits make it easier to compare revisions, revert an experiment, review the final fix, and use bisection. Keep tests close to the behavior they protect and make regression tests deterministic when possible.

Common Debugging Pitfalls

PitfallWhy it causes problemsPreferred practice
Changing multiple things before rerunningYou cannot determine which change affected the result.Change one relevant factor and record the experiment.
Assuming the error message is the root causeThe message often identifies the immediate failure, not where the invalid state began.Trace inputs and state backward through the call path.
Debugging without a reproducible caseResults become anecdotal and difficult to verify.Collect reproduction steps, environment details, and evidence.
Using only print statementsScattered output can obscure timing, state, and call relationships.Use an interactive debugger when stepping and state inspection are more suitable.
Fixing only the symptomThe same underlying defect may appear through another path.Identify and correct the root cause, then add regression coverage.
Leaving sensitive or excessive logging in productionLogs can expose secrets, create privacy risks, and increase operational cost.Use safe structured context, appropriate severity, redaction, and cleanup.

Debugging Checklist

  • Can I state the symptom, expected behavior, and actual behavior clearly?
  • Do I have repeatable reproduction steps?
  • Can I reduce the issue to a minimal reproducible example?
  • What do the error message, exception type, error code, or stack trace actually establish?
  • What inputs, variables, logs, and environment facts provide evidence?
  • What is my current hypothesis, and what experiment could disprove it?
  • Where was the invalid state introduced?
  • Have I changed only one relevant factor at a time?
  • Does the targeted fix address the root cause?
  • Does the original failing case now pass?
  • Have I tested edge cases and adjacent behavior?
  • Did I add a regression test and remove unsafe temporary diagnostics?