Zum Inhalt springen

Git Bisect und fortgeschrittene Debugging-Tools

Wie du git bisect, bedingte Breakpoints und systematische Debugging-Techniken nutzt, um Bugs in Minuten statt Stunden zu finden.

5 Min. Lesezeit
Git-bisect-Ausgabe, die eine Binärsuche durch den Commit-Verlauf zeigt

Der teuerste Teil eines Bugs ist, ihn zu finden. Wenn du weißt, welche Zeile falsch ist, ist der Fix meistens offensichtlich. Trotzdem debuggen die meisten Entwickler, indem sie Code anstarren, console.logs hinzufügen und hoffen, dass ihnen etwas auffällt. Diese Techniken skalieren schlecht — sie funktionieren bei trivialen Bugs, aber scheitern, wenn das Problem mehrere Dateien, Commits oder Services überspannt.

Systematische Debugging-Tools können eine 4-stündige Suche in einen 15-minütigen Prozess verwandeln. Das am meisten unterschätzte davon ist git bisect.

Git Bisect: Binärsuche durch den Verlauf

Wenn ein Bug jetzt existiert, letzte Woche aber noch nicht, findet git bisect den genauen Commit, der ihn eingeführt hat. Es führt eine Binärsuche durch deine Commit-Historie durch und halbiert den Suchraum bei jedem Schritt.

shbash
# Start bisecting
git bisect start
 
# Mark the current commit as broken
git bisect bad
 
# Mark a known-good commit (e.g., last week's release)
git bisect good v2.3.0
 
# Git checks out a middle commit — test it
# If the bug exists:
git bisect bad
# If the bug does NOT exist:
git bisect good
 
# Repeat 5-7 times for 100+ commits
# Git narrows down to the exact problem commit

Bei 1000 Commits braucht bisect maximal 10 Schritte (log₂ 1000 ≈ 10). Vergleich das damit, Commits manuell nacheinander zu prüfen.

Automatisiertes Bisect

Wenn du einen Test hast, der den Bug reproduziert, kann bisect vollständig automatisiert laufen:

shbash
# Automated bisect — runs a test script at each step
git bisect start HEAD v2.3.0
git bisect run npm test -- --grep "payment calculation"
 
# Or with a custom script
git bisect run ./scripts/check-bug.sh
shbash
#!/usr/bin/env bash
# check-bug.sh — exit 0 = good, exit 1 = bad
set -euo pipefail
 
npm run build 2>/dev/null
# Test the specific behavior that's broken
result=$(node -e "
  const { calculateTotal } = require('./dist/pricing');
  const total = calculateTotal([{ price: 10, qty: 3 }], 0.1);
  process.exit(total === 27 ? 0 : 1);
")

Schreibe das Testskript so, dass es bei "good" 0 und bei "bad" einen Wert ungleich 0 zurückgibt. Git bisect erledigt den Rest — du kommst zurück und findest den genauen Commit.

Bedingte Breakpoints

Die meisten Entwickler setzen Breakpoints und steppen durch jede Iteration. Bei einer Schleife mit 10,000 Durchläufen, in der der Bug bei Iteration 8,437 auftritt, ist steppen keine Option.

tstypescript
// Instead of a regular breakpoint, use a conditional one
// In Chrome DevTools: right-click breakpoint → "Edit breakpoint"
// Condition: item.id === 'problematic-id'
 
// Or use programmatic breakpoints
function processItems(items: Item[]) {
  for (const item of items) {
    // This breakpoint only triggers when the condition is true
    if (item.price < 0) {
      debugger; // Only pauses on the problematic item
    }
 
    const result = calculateDiscount(item);
    applyResult(result);
  }
}
tstypescript
// ❌ Console.log bombing — noisy and manual
function processOrder(order: Order) {
  console.log('order', order);
  console.log('items', order.items);
  console.log('total', order.total);
  order.items.forEach((item, i) => {
    console.log(`item ${i}`, item);
    console.log(`price ${i}`, item.price);
  });
}
 
// ✅ Targeted conditional logging
function processOrder(order: Order) {
  // Only log when the total doesn't match expected
  const expected = order.items.reduce((sum, i) => sum + i.price * i.qty, 0);
  if (Math.abs(order.total - expected) > 0.01) {
    console.table(order.items);
    console.log('Expected:', expected, 'Got:', order.total);
    debugger;
  }
}

Stack-Trace-Analyse

Wenn ein Fehler einen Stack Trace enthält, lohnt es sich, ihn systematisch zu lesen, statt zu raten. Stack Traces liest man von unten nach oben — die Ursache steht unten, das Symptom oben.

tstypescript
// Error: Cannot read property 'name' of undefined
//     at formatUser (src/formatters.ts:42)     ← symptom
//     at processUsers (src/handlers.ts:18)      ← where it's called
//     at fetchAndProcess (src/api.ts:67)        ← the data source
//     at handleRequest (src/server.ts:23)       ← entry point
 
// The bug is likely in fetchAndProcess or handleRequest
// — something returns undefined where a user was expected.
// formatUser is innocent — it received bad input.

Bei asynchronen Stack Traces, die über awaits Kontext verlieren, verwende das Flag --async-stack-traces in Node.js oder aktiviere das "Async"-Häkchen in den Chrome DevTools.

tstypescript
// Preserve context in async error handling
async function fetchUser(id: string): Promise<User> {
  try {
    const response = await api.get(`/users/${id}`);
    return response.data;
  } catch (error) {
    // Wrap with context instead of re-throwing bare error
    throw new Error(
      `Failed to fetch user ${id}: ${error instanceof Error ? error.message : 'Unknown error'}`,
      { cause: error }
    );
  }
}

Die Option { cause: error } (ES2022) erhält die ursprüngliche Fehlerkette und liefert dir sowohl den Geschäftskontext als auch die technischen Details.

Systematischer Debugging-Prozess

Zufälliges Debugging verschwendet Zeit. Ein strukturierter Prozess führt schneller zur Antwort.

tstypescript
interface DebuggingProcess {
  steps: [
    "1. Reproduce — can you trigger the bug reliably?",
    "2. Isolate — what's the smallest input that causes it?",
    "3. Narrow — binary search the code (bisect, comment out halves)",
    "4. Hypothesize — form a theory about the root cause",
    "5. Test — verify the theory with a targeted experiment",
    "6. Fix — change the minimum code necessary",
    "7. Verify — confirm the fix AND that nothing else broke",
  ];
}

Schritt 2 — Isolation — ist der Punkt, an dem die meisten Entwickler vorausellen. "Die App stürzt ab, wenn ich diesen Workflow ausführe" auf "Diese Funktion gibt null zurück, wenn ein leeres Array übergeben wird" zu reduzieren, eliminiert 90% des Rauschens.

shbash
# Isolate network issues with curl
# Instead of "the API is broken," narrow it down:
curl -v https://api.example.com/health   # Server reachable?
curl -v https://api.example.com/users/1  # Specific endpoint?
curl -v -H "Authorization: Bearer $TOKEN" https://api.example.com/users/1  # Auth issue?

Debugging von Speicher und Performance

Performance-Bugs erfordern andere Tools. Der Performance-Tab der Chrome DevTools und --inspect in Node.js decken die meisten Szenarien ab.

tstypescript
// Quick performance measurement
function measureExecution<T>(label: string, fn: () => T): T {
  const start = performance.now();
  const result = fn();
  const duration = performance.now() - start;
 
  if (duration > 100) {
    console.warn(`⚠️ ${label} took ${duration.toFixed(2)}ms`);
  }
  return result;
}
 
// Memory leak detection pattern
function detectLeaks() {
  const before = process.memoryUsage().heapUsed;
 
  // Run the suspected operation multiple times
  for (let i = 0; i < 1000; i++) {
    suspectedLeakyFunction();
  }
 
  // Force garbage collection (run node with --expose-gc)
  if (global.gc) global.gc();
 
  const after = process.memoryUsage().heapUsed;
  const leaked = after - before;
 
  if (leaked > 1024 * 1024) {
    console.warn(`Potential leak: ${(leaked / 1024 / 1024).toFixed(2)} MB`);
  }
}

Debugging in der Produktion

Debugging in der Produktion erfordert nicht-invasive Techniken. Du kannst keine Breakpoints setzen oder console.logs auf einem Produktionsserver hinzufügen.

tstypescript
// Structured logging — queryable in log aggregation tools
import { logger } from './logger';
 
async function processPayment(payment: Payment) {
  const correlationId = crypto.randomUUID();
 
  logger.info('payment.started', {
    correlationId,
    amount: payment.amount,
    currency: payment.currency,
    userId: payment.userId,
  });
 
  try {
    const result = await gateway.charge(payment);
    logger.info('payment.completed', {
      correlationId,
      transactionId: result.id,
      duration: result.duration,
    });
    return result;
  } catch (error) {
    logger.error('payment.failed', {
      correlationId,
      error: error instanceof Error ? error.message : 'Unknown',
      stack: error instanceof Error ? error.stack : undefined,
    });
    throw error;
  }
}

Die correlationId verknüpft alle Log-Einträge einer einzelnen Operation. Wenn ein Benutzer meldet "meine Zahlung ist fehlgeschlagen", suchst du in den Logs nach der correlationId und siehst die gesamte Zeitlinie der Operation.

Wichtige Erkenntnisse

  1. git bisect findet Regressions-Commits in Minuten — automatisiere es mit einem Testskript für eine mühelose Binärsuche durch die Historie
  2. Bedingte Breakpoints schlagen console.log — lösen nur im problematischen Fall aus, statt durch Tausende von Iterationen zu steppen
  3. Lies Stack Traces von unten nach oben — die Ursache steht unten, das Symptom oben
  4. Isoliere, bevor du untersuchst — reduziere das Problem auf die kleinste reproduzierende Eingabe, bevor du dich in den Code vertiefst
  5. Verwende strukturiertes Logging in der Produktion — correlationIds machen es möglich, eine einzelne Operation über Services hinweg zu verfolgen
  6. Wrappe Fehler mit Kontext — { cause: error } erhält die vollständige Fehlerkette für das Debugging
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX