Heisenbug: Was es ist, warum es auftritt und Methoden zur Fehlererkennung

Autor: IT Sectr Veröffentlicht: 2026-07-29 Lesezeit: 10 Min.

Heisenbug — ein Fehler, der beim Versuch, ihn zu debuggen, verschwindet. Der Begriff stammt von Heisenbergs Unschärferelation: Beobachtung beeinflusst das Verhalten des Systems. In der mobilen Entwicklung ist Heisenbug eines der schwierigsten Probleme, da Standard-Debugging-Methoden (Logs, Breakpoints, zusätzlicher Code) den Programmzustand ändern und den Fehler verbergen. Lassen Sie uns die Ursachen und Methoden zum Umgang mit schwer fassbaren Fehlern untersuchen.

Wichtige Erkenntnisse

  • Race condition — die Hauptursache für Heisenbug: Änderung des Timings beim Debuggen maskiert das Problem
  • Bohrbug — ein vorhersagbarer Fehler, im Gegensatz zu Heisenbug leicht reproduzierbar
  • Mandelbug — ein Fehler mit komplexen Ursache-Wirkungs-Beziehungen, empfindlich gegenüber Anfangsbedingungen
  • ThreadSanitizer — ein Tool zur Erkennung von Datenrennen, das das Timing nicht beeinflusst
  • Deterministische Tests — der einzige zuverlässige Weg, einen Heisenbug zu reproduzieren

Was ist ein Heisenbug in der mobilen Entwicklung?

Heisenbug — eine Klasse von Fehlern, die in der Produktion oder im normalen Betrieb auftreten, aber beim Versuch, sie in einer Debugging-Umgebung zu reproduzieren, verschwinden. Der Begriff wurde in den 1980er Jahren vom Programmierer Jim Gray im Kontext verteilter Systeme geprägt, ist aber heute aufgrund ihrer asynchronen Natur am relevantesten für mobile Anwendungen.

Der Hauptgrund: Standard-Debugging-Tools verändern die Ausführungsumgebung. Ein Breakpoint pausiert den Thread für mehrere Millisekunden, Logging fügt synchrone E/A hinzu, zusätzliche Prüfungen ändern die Reihenfolge der Operationen. In einer Multithread-Umgebung kann bereits eine Mikrosekunde Verzögerung die Ausführungsreihenfolge der Threads ändern und ein Datenrennen verbergen.

Laut Microsoft Research (2022) werden etwa 15-25% aller Fehler in Multithread-Mobilanwendungen als Heisenbug klassifiziert. Gleichzeitig ist die Zeit zum Auffinden und Beheben eines Heisenbugs aufgrund der fehlenden direkten Reproduzierbarkeit durchschnittlich 5-10 Mal länger als bei einem normalen Fehler.

Heisenbug-Beispiel

Eine App stürzt in der Produktion beim schnellen Wischen durch eine Liste ab, aber beim Verbinden mit dem Debugger oder Hinzufügen von Logs — funktioniert sie einwandfrei. Ursache: ein Datenrennen zwischen dem UI-Thread (Aktualisierung von RecyclerView) und dem Hintergrundthread (Aktualisierung der Adapterdaten). Logs fügen eine Verzögerung hinzu, die die Threads zufällig synchronisiert.

Bohrbug, Mandelbug, Heisenbug: Fehlerklassifikation

Bohrbug — ein vorhersagbarer, stabil reproduzierbarer Fehler. Benannt in Analogie zum Bohrschen Atommodell: wie ein Atom verhält sich der Fehler bei jeder Beobachtung gleich. Beispiel: NullPointerException beim Klicken auf eine Schaltfläche vor dem Laden der Daten. Wird mit Standard-Komponententests behandelt.

Mandelbug — ein Fehler mit einer komplexen, chaotischen Ursache-Wirkungs-Beziehung (benannt in Analogie zur Mandelbrot-Menge). Tritt nur unter einer bestimmten Kombination von Bedingungen auf: OS-Version, Gerätemodell, Netzwerkstatus. Unterscheidet sich von Heisenbug dadurch, dass er beim Debuggen nicht verschwindet — das Problem ist die Schwierigkeit der Reproduktion, nicht die Verhaltensänderung durch Tools.

Heisenbug — ein Fehler, der genau wegen der Debugging-Tools verschwindet. Wenn Sie ein Log hinzufügen — verschwindet der Fehler. Wenn Sie einen Breakpoint setzen — tritt der Fehler nicht auf. Wenn Sie alles entfernen — kommt der Fehler zurück. Hauptursache: verändertes Timing beim Debuggen.

TypReproduzierbarkeitReaktion auf DebuggingBeispiel
Bohrbug100%UnverändertNPE bei leerer Liste
MandelbugChaotischUnverändertAbsturz auf Android 12, Samsung, niedrigem Akku
HeisenbugNur ohne DebuggingVerschwindetRace Condition, die mit Logs verschwindet
SchrödinbugManifestiert sich nicht im CodeErscheint beim HinsehenFehler im Code sichtbar, löst aber nie aus

Hauptursachen für Heisenbug

Race condition — die häufigste Ursache für Heisenbug. Zwei Threads greifen ohne Synchronisation auf gemeinsame Daten zu. Der Debugger führt eine Verzögerung ein, wodurch die Threads auf natürliche Weise synchronisiert werden. Ohne Debugger ist die Ausführungsreihenfolge unvorhersehbar.

Timing-abhängige Fehler — Fehler, die nur bei einer bestimmten Ausführungsgeschwindigkeit auftreten. Zum Beispiel eine Animation, die abgeschlossen sein muss, bevor die nächste Operation beginnt. Im Debugger läuft die Animation langsamer, und die Operation kann nach Abschluss der Animation beginnen. In der Produktion — umgekehrt.

kotlin
// Example race condition — typical Heisenbug
class ListViewModel : ViewModel() {
    private var items = mutableListOf<String>()

    fun loadFromNetwork() {
        viewModelScope.launch(Dispatchers.IO) {
            val result = api.fetchItems()
            items.addAll(result) // ❌ Not thread-safe
        }
    }

    fun getItems(): List<String> = items.toList()
    // Race condition: getItems read may overlap with loadFromNetwork write
}

Compileroptimierung — der Compiler (JIT, ART, Kotlin/Native) kann Anweisungen zur Optimierung umordnen. In einem Debug-Build sind Optimierungen deaktiviert und der Code wird «wie geschrieben» ausgeführt. In einem Release-Build ändert der Compiler die Reihenfolge der Operationen, was versteckte Annahmen im Code aufdecken kann.

  • ThreadLocal — falsche Verwendung von thread-lokalen Variablen, die für andere Threads nicht sichtbar sind
  • Nicht initialisierte Variablen — Code, der sich auf Standardwerte von Klassenfeldern verlässt
  • GCD/dispatch-Warteschlangen — in iOS, undefinierte Reihenfolge der Blockausführung in nebenläufigen Warteschlangen
  • Gepufferte E/A — Daten werden erst auf die Festplatte geschrieben, wenn der Puffer voll ist

Strategien zum Auffinden schwer fassbarer Fehler

ThreadSanitizer (TSan) — ein Google-Tool zur Erkennung von Datenrennen in C/C++ und Kotlin/Native. Es wird in den Build eingebettet und erkennt jeden Zugriff auf gemeinsam genutzten Speicher ohne Synchronisation. Im Gegensatz zu Logs beeinflusst TSan das Timing nicht, da es durch instrumentierten Code und nicht durch E/A arbeitet.

Deterministische Tests — ersetzen Sie echte Asynchronität durch kontrollierte Asynchronität. Verwenden Sie TestDispatcher (Kotlin), RxJava Plugins oder GCD-Testwarteschlangen (iOS) zur vollständigen Kontrolle der Ausführungsreihenfolge. Geben Sie konkrete Szenarien an: Thread A läuft, dann B, dann wieder A.

Zyklisches Logging — Protokollierung in einen Ringpuffer im Speicher (nicht auf der Festplatte). Wenn der Fehler auftritt, wird der Puffer in eine Datei gespeichert. Da das Schreiben in den Speicher Nanosekunden dauert (statt Millisekunden für Festplatten-E/A), beeinflusst ein solches Logging das Timing nicht und maskiert den Heisenbug nicht.

kotlin
class CyclicBuffer(val capacity: Int = 1000) {
    private val buffer = ArrayDeque<String>(capacity)
    private val lock = Any()

    fun log(message: String) {
        synchronized(lock) {
            if (buffer.size >= capacity) buffer.removeFirst()
            buffer.addLast(message)
        }
    }

    fun flush() {
        synchronized(lock) { buffer.forEach { fileWriter.write(it) } }
    }
}

Logging in der Produktion — wenn der Fehler lokal nicht reproduzierbar ist, sammeln Sie Daten in der Produktion. Verwenden Sie Firebase Crashlytics-Logs, Sentry Breadcrumbs oder einen benutzerdefinierten zyklischen Logger. Wichtig: Das Logging sollte asynchron sein und die Leistung minimal beeinträchtigen.

Heisenbug-Prävention auf Architekturebene

Zustandsisolation — minimieren Sie gemeinsam genutzten veränderlichen Zustand. Jede Komponente sollte ihren eigenen isolierten Zustand haben, der für direkte Schreibzugriffe von anderen Komponenten unzugänglich ist. Verwenden Sie Unidirectional Data Flow (UDF) — der Zustand fließt in eine Richtung: Ereignis → Reducer → Zustand → UI.

Funktionaler Ansatz — reine Funktionen ohne Seiteneffekte sind einfacher zu testen und zu debuggen. Isolieren Sie Seiteneffekte (Netzwerk, DB, Dateien) in streng definierten Schichten (Repository, Datenquelle). Threading-Fehler in funktionalem Code sind praktisch unmöglich.

Strict Mode — aktivieren Sie Android StrictMode im Debug-Build. Er erkennt Verstöße gegen die Threading-Richtlinie (Netzwerk auf dem Hauptthread, Festplatten-E/A auf dem Hauptthread) und wirft eine Ausnahme. Dies verwandelt einen potenziellen Heisenbug in einen deterministischen Bohrbug, der sofort sichtbar ist.

kotlin
class DebugApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            StrictMode.setThreadPolicy(
                StrictMode.ThreadPolicy.Builder()
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()
                    .penaltyLog()
                    .build()
            )
        }
    }
}

Code-Review mit Fokus auf Asynchronität — ein obligatorischer Teil des Prozesses. Jeder Pull-Request sollte auf gemeinsam genutzten veränderlichen Zustand, nicht threadsichere Sammlungen und fehlende Synchronisation überprüft werden. Verwenden Sie Lint-Regeln, um bestimmte Muster automatisch zu verbieten (z. B. Zugriff auf MutableList ohne synchronized).

Häufig gestellte Fragen

Warum ist Heisenbug so schwer zu finden?

Weil Standardmethoden — Breakpoints, Logs, print — die Ausführungsumgebung so stark verändern, dass der Fehler nicht mehr auftritt. Der Debugger pausiert alle Threads für Dutzende von Millisekunden. Während dieser Zeit löst sich die Race Condition, die den Fehler verursacht hat, auf natürliche Weise. Benötigt werden Tools, die das Ausführungs-Timing nicht beeinflussen.

Wie unterscheidet sich Heisenbug von Mandelbug?

Mandelbug ist aufgrund der Komplexität der Bedingungen schwer zu reproduzieren, aber Debugging-Tools beeinflussen seine Manifestation nicht. Heisenbug hingegen verschwindet genau wegen der Debugging-Tools. Beispiel für Mandelbug: ein Absturz nur auf Geräten mit Android 11, 3 GB RAM und einem Akkustand unter 15%. Beispiel für Heisenbug: eine Race Condition, die beim Hinzufügen von Log.d() verschwindet.

Wie testet man Heisenbug in CI/CD?

Verwenden Sie Flaky-Test-Erkennung — Tests, die manchmal fehlschlagen, manchmal bestehen. Verwenden Sie in Android den Android Test Orchestrator zur Testisolierung. Fügen Sie StrictMode zu Debug-Tests hinzu. Instrumentieren Sie den Build mit ThreadSanitizer. Wenn ein Test in >5% der Läufe flaky ist — betrachten Sie ihn als potenziellen Heisenbug und untersuchen Sie ihn vor dem Merge.

Helfen Flow/Coroutines, Heisenbug zu vermeiden?

Teilweise. Flow und strukturierte Nebenläufigkeit in Kotlin reduzieren die Menge an gemeinsam genutztem veränderlichem Zustand und vereinfachen die Thread-Verwaltung. Aber Koroutinen garantieren keine Thread-Sicherheit: Wenn zwei Koroutinen Zustand teilen, ist eine Race Condition immer noch möglich. Verwenden Sie Mutex zum Schutz des gemeinsamen Zustands oder Channel zur Datenübergabe zwischen Koroutinen.

Was tun, wenn Heisenbug nur in der Produktion auftritt?

Verwenden Sie einen zyklischen Log-Puffer im Speicher mit automatischem Leeren bei Fehlern. Fügen Sie detailliertes Monitoring über Crashlytics oder Sentry mit benutzerdefinierten Breadcrumbs hinzu. Aktivieren Sie für Android die ANR-Erkennung und prüfen Sie Traces. Wenn der Fehler eine Race Condition ist, kann ThreadSanitizer in einem Debug-Build mit produktionsähnlicher Last das Problem aufdecken.

Zusammenfassung

  • Heisenbug — ein Fehler, der beim Debuggen verschwindet; Hauptursache sind Timing-Änderungen durch Entwickler-Tools
  • Race condition — die Hauptursache für Heisenbug in mobilen Apps, besonders in asynchronem Code
  • Bohrbug (100% reproduzierbar) und Mandelbug (chaotisch) — andere Fehlertypen, nicht mit Heisenbug verwechseln
  • ThreadSanitizer — das beste Tool zur Erkennung von Datenrennen, ohne Beeinflussung des Ausführungs-Timings
  • Zyklisches Logging im Speicher statt auf der Festplatte — eine Möglichkeit, Daten zu sammeln, ohne Heisenbug zu maskieren
  • Unidirectional Data Flow und Minimierung von gemeinsam genutztem veränderlichem Zustand — architektonische Prävention einer ganzen Fehlerklasse
  • StrictMode im Debug-Build verwandelt einen potenziellen Heisenbug in einen deterministischen Bohrbug, der sofort sichtbar ist

Wir entwickeln eine mobile Applikation schlüsselfertig

IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.

Projekt besprechen

Lesen Sie auch