Heisenbug: qué es, por qué ocurre y métodos para detectarlo

Autor: IT Sectr Publicado: 2026-07-29 Tiempo de lectura: 10 min

Heisenbug — un error que desaparece al intentar depurarlo. El término proviene del principio de incertidumbre de Heisenberg: la observación afecta el comportamiento del sistema. En el desarrollo móvil, Heisenbug es uno de los problemas más difíciles porque los métodos estándar de depuración (logs, breakpoints, código adicional) cambian el estado del programa y ocultan el error. Analicemos las causas y los métodos para combatir errores esquivos.

Puntos Clave

  • Race condition — la causa principal de Heisenbug: cambiar el timing durante la depuración enmascara el problema
  • Bohrbug — un error predecible, fácilmente reproducible a diferencia de Heisenbug
  • Mandelbug — un error con relaciones causales complejas, sensible a las condiciones iniciales
  • ThreadSanitizer — una herramienta para detectar carreras de datos que no afecta el timing
  • Pruebas deterministas — la única forma fiable de reproducir un Heisenbug

¿Qué es un Heisenbug en el desarrollo móvil?

Heisenbug — una clase de errores que se manifiestan en producción o durante el funcionamiento normal, pero desaparecen al intentar reproducirlos en un entorno de depuración. El término fue acuñado en la década de 1980 por el programador Jim Gray en el contexto de sistemas distribuidos, pero hoy es más relevante para aplicaciones móviles debido a su naturaleza asíncrona.

La razón principal: las herramientas de depuración estándar cambian el entorno de ejecución. Un breakpoint pausa el hilo durante varios milisegundos, el logging añade E/S síncrona, las comprobaciones adicionales cambian el orden de las operaciones. En un entorno multihilo, incluso un microsegundo de retraso puede alterar el orden de ejecución de los hilos y ocultar una carrera de datos.

Según Microsoft Research (2022), alrededor del 15-25% de todos los errores en aplicaciones móviles multihilo se clasifican como Heisenbug. Al mismo tiempo, el tiempo para encontrar y corregir un Heisenbug es en promedio de 5 a 10 veces mayor que para un error normal, debido a la imposibilidad de reproducirlo directamente.

Ejemplo de Heisenbug

Una aplicación falla en producción al deslizar rápidamente una lista, pero al conectarse al depurador o agregar logs — funciona perfectamente. Causa: una carrera de datos entre el hilo de UI (actualizando RecyclerView) y el hilo de fondo (actualizando los datos del adaptador). Los logs agregan un retraso que sincroniza los hilos aleatoriamente.

Bohrbug, Mandelbug, Heisenbug: Clasificación de errores

Bohrbug — un error predecible, establemente reproducible. Nombrado por analogía con el modelo atómico de Bohr: como un átomo, el error se comporta igual cada vez que se observa. Ejemplo: NullPointerException al hacer clic en un botón antes de que se carguen los datos. Se trata con pruebas unitarias estándar.

Mandelbug — un error con una relación causal compleja y caótica (nombrado por analogía con el conjunto de Mandelbrot). Se manifiesta solo bajo una cierta combinación de condiciones: versión del SO, modelo de dispositivo, estado de la red, fase lunar. Se diferencia de Heisenbug en que no desaparece durante la depuración — el problema es la dificultad de reproducción, no el cambio de comportamiento por las herramientas.

Heisenbug — un error que desaparece precisamente por las herramientas de depuración. Si añades un log — el error desaparece. Si pones un breakpoint — el error no se manifiesta. Si eliminas todo — el error regresa. La causa principal: alteración del timing durante la depuración.

TipoReproducibilidadReacción a la depuraciónEjemplo
Bohrbug100%Sin cambiosNPE en lista vacía
MandelbugCaóticaSin cambiosFallo en Android 12, Samsung, batería baja
HeisenbugSolo sin depuraciónDesapareceRace condition que desaparece con logs
SchrödinbugNo se manifiesta en códigoAparece al mirarloError visible en código pero nunca se activa

Principales causas del Heisenbug

Race condition — la causa número uno de Heisenbug. Dos hilos acceden a datos compartidos sin sincronización. El depurador introduce un retraso, lo que hace que los hilos se sincronicen naturalmente. Sin el depurador, el orden de ejecución es impredecible.

Errores dependientes del timing — errores que se manifiestan solo a una cierta velocidad de ejecución. Por ejemplo, una animación que debe terminar antes de que comience la siguiente operación. En el depurador, la animación va más lenta y la operación tiene tiempo de comenzar después de que termine la animación. En producción — al revés.

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
}

Optimización del compilador — el compilador (JIT, ART, Kotlin/Native) puede reordenar instrucciones para optimizar. En una compilación de depuración (debug build), las optimizaciones están desactivadas y el código se ejecuta «tal como está escrito». En una compilación de lanzamiento (release build), el compilador cambia el orden de las operaciones, lo que puede revelar suposiciones ocultas en el código.

  • ThreadLocal — uso incorrecto de variables thread-local que no son visibles para otros hilos
  • Variables no inicializadas — código que depende de valores predeterminados de campos de clase
  • Colas GCD/dispatch — en iOS, orden indefinido de ejecución de bloques en colas concurrentes
  • E/S con búfer — los datos no se escriben en disco hasta que el búfer esté lleno

Estrategias para detectar errores esquivos

ThreadSanitizer (TSan) — una herramienta de Google para detectar carreras de datos en C/C++ y Kotlin/Native. Se integra en la compilación y detecta cualquier acceso a memoria compartida sin sincronización. A diferencia de los logs, TSan no afecta el timing porque funciona a través de código instrumentado, no de E/S.

Pruebas deterministas — reemplace la asincronía real por asincronía controlada. Use TestDispatcher (Kotlin), RxJava Plugins o colas de prueba GCD (iOS) para tener control total sobre el orden de ejecución. Especifique escenarios concretos: el hilo A se ejecuta, luego B, luego A nuevamente.

Logging cíclico — registro en un búfer circular en memoria (no en disco). Cuando ocurre el error, el búfer se guarda en un archivo. Dado que escribir en memoria toma nanosegundos (en lugar de milisegundos para E/S de disco), dicho registro no afecta el timing ni enmascara el Heisenbug.

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 en producción — si el error no se reproduce localmente, recolecte datos en producción. Use Firebase Crashlytics logs, Sentry Breadcrumbs o un registrador cíclico personalizado. Importante: el logging debe ser asíncrono y tener un mínimo impacto en el rendimiento.

Prevención de Heisenbug a nivel de arquitectura

Aislamiento de estado — minimice el estado mutable compartido. Cada componente debe tener su propio estado aislado, inaccesible para escritura directa desde otros componentes. Use Unidirectional Data Flow (UDF) — el estado fluye en una dirección: Evento → Reductor → Estado → UI.

Enfoque funcional — las funciones puras sin efectos secundarios son más fáciles de probar y depurar. Aisle los efectos secundarios (red, BD, archivos) en capas estrictamente definidas (repositorio, fuente de datos). Los errores de concurrencia en código funcional son prácticamente imposibles.

Modo estricto — active Android StrictMode en la compilación de depuración. Detecta violaciones de la política de hilos (red en el hilo principal, E/S de disco en el hilo principal) y lanza una excepción. Esto convierte un posible Heisenbug en un Bohrbug determinista que es visible de inmediato.

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

Revisión de código con enfoque en asincronía — una parte obligatoria del proceso. Cada pull request debe verificarse en busca de estado mutable compartido, colecciones no seguras para hilos y falta de sincronización. Use reglas lint para prohibir automáticamente ciertos patrones (por ejemplo, acceder a MutableList sin synchronized).

Preguntas Frecuentes

¿Por qué es tan difícil encontrar un Heisenbug?

Porque los métodos estándar — breakpoints, logs, print — cambian tanto el entorno de ejecución que el error deja de manifestarse. El depurador pausa todos los hilos durante decenas de milisegundos. Durante este tiempo, la carrera de datos que causó el error se resuelve naturalmente. Se necesitan herramientas que no afecten el timing de ejecución.

¿En qué se diferencia Heisenbug de Mandelbug?

Mandelbug es difícil de reproducir debido a la complejidad de las condiciones, pero las herramientas de depuración no afectan su manifestación. Heisenbug, en cambio, desaparece precisamente por las herramientas de depuración. Ejemplo de Mandelbug: un fallo solo en dispositivos con Android 11, 3 GB de RAM y nivel de batería inferior al 15%. Ejemplo de Heisenbug: una carrera de datos que desaparece al añadir Log.d().

¿Cómo probar Heisenbug en CI/CD?

Use detección de pruebas flaky — pruebas que a veces fallan, a veces pasan. En Android, use Android Test Orchestrator para aislar pruebas. Agregue StrictMode a las pruebas de depuración. Instrumente la compilación con ThreadSanitizer. Si una prueba es flaky >5% de las ejecuciones — considérela un posible Heisenbug e investigue antes de fusionar.

¿Flow/Coroutines ayudan a evitar Heisenbug?

Parcialmente. Flow y la concurrencia estructurada en Kotlin reducen la cantidad de estado mutable compartido y simplifican la gestión de hilos. Pero las corrutinas no garantizan la seguridad en hilos: si dos corrutinas comparten estado, aún es posible una carrera de datos. Use Mutex para proteger el estado compartido o Channel para pasar datos entre corrutinas.

¿Qué hacer si Heisenbug solo se manifiesta en producción?

Use un búfer de registro cíclico en memoria con vaciado automático en caso de error. Agregue monitoreo detallado a través de Crashlytics o Sentry con breadcrumbs personalizados. Para Android, active la detección de ANR y revise los traces. Si el error es una carrera de datos, ThreadSanitizer en una compilación de depuración con carga similar a producción puede revelar el problema.

Resumen

  • Heisenbug — un error que desaparece al intentar depurarlo; la causa principal son los cambios de timing por las herramientas del desarrollador
  • Race condition — la causa principal de Heisenbug en aplicaciones móviles, especialmente en código asíncrono
  • Bohrbug (100% reproducible) y Mandelbug (caótico) — otros tipos de errores, no confundir con Heisenbug
  • ThreadSanitizer — la mejor herramienta para detectar carreras de datos, sin afectar el timing de ejecución
  • Logging cíclico en memoria en lugar de disco — una forma de recolectar datos sin enmascarar Heisenbug
  • Unidirectional Data Flow y minimizar el estado mutable compartido — prevención arquitectónica de toda una clase de errores
  • StrictMode en compilaciones de depuración convierte un posible Heisenbug en un Bohrbug determinista, visible de inmediato

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también