Debugging y troubleshooting: conceptos esenciales para resolver problemas de software

Debugging y troubleshooting: conceptos esenciales para resolver problemas de software

Cuando un programa falla, el problema no siempre está donde aparece el error. Debugging y troubleshooting son prácticas fundamentales para localizar la causa de un problema, entender qué está ocurriendo y aplicar una solución verificable.

En este artículo veremos qué significan ambos conceptos, cómo diferenciarlos y un proceso práctico para resolver errores de software.

¿Qué es debugging?

Debugging es el proceso de localizar, analizar y corregir errores dentro del código de un programa.

Un error puede provocar que el programa termine inesperadamente, produzca resultados incorrectos, se comporte de manera diferente a lo esperado, genere excepciones, o funcione lentamente y consuma recursos de forma excesiva.

La idea fundamental es sencilla:

Problema observado -> Evidencia -> Hipótesis -> Prueba -> Corrección -> Verificación

El debugging no consiste simplemente en cambiar código hasta que "funcione". El objetivo es identificar la causa del comportamiento incorrecto.

¿Qué es troubleshooting?

Troubleshooting es un proceso más amplio para diagnosticar y resolver problemas en un sistema. Puede incluir problemas de código, configuración, sistema operativo, red, base de datos, dependencias, hardware, servicios externos y variables de entorno.

Por ejemplo, una aplicación puede mostrar:

Error: Connection refused

El problema podría estar en el código, pero también podría deberse a que el servidor de base de datos está detenido, el puerto es incorrecto o una regla de firewall está bloqueando la conexión.

Por eso, debugging suele concentrarse en el comportamiento interno del software, mientras que troubleshooting analiza el sistema completo.

Reproducir el error

Antes de modificar el código, intenta reproducir el problema.

Preguntas importantes:

  • ¿Qué acción provoca el error?
  • ¿Ocurre siempre?
  • ¿Ocurre solamente con determinados datos?
  • ¿Desde cuándo ocurre?
  • ¿Qué cambió antes de que apareciera?
  • ¿Cuál era el comportamiento esperado?
  • ¿Cuál es el comportamiento real?

Un error reproducible es mucho más fácil de investigar porque permite comparar diferentes estados del programa.

Leer el error antes de cambiar el código

Los mensajes de error proporcionan información valiosa. Ejemplo:

Traceback (most recent call last):
  File "app.py", line 8, in <module>
    total = price * quantity
TypeError: can't multiply sequence by non-int of type 'float'

El error indica que Python intentó realizar una operación incompatible. Una posible causa sería:

price = "100"
quantity = 2.5

El valor de price es texto, no un número. Una corrección podría ser:

price = float("100")
quantity = 2.5

total = price * quantity

La lección importante es no ignorar el mensaje de error. Primero hay que interpretarlo.

Hipótesis y evidencia

Una de las habilidades más importantes del debugging es evitar asumir la causa.

Supongamos que una aplicación tarda demasiado. Una mala estrategia sería suponer que la base de datos seguramente está lenta. Una estrategia de debugging sería comprobarlo:

start = time.time()

result = database.query()

print("Database:", time.time() - start)

process(result)

print("Total:", time.time() - start)

Ahora existe evidencia para determinar cuánto tiempo consume cada parte. El principio es:

No supongas -> mide -> compara -> concluye

Debugging con breakpoints

Los debuggers permiten detener la ejecución del programa en puntos específicos llamados breakpoints. Ejemplo conceptual:

def calculate_total(price, quantity):
    total = price * quantity
    return total

Podemos colocar un breakpoint antes de calcular total y observar price, quantity y total. Esto permite inspeccionar el estado real del programa sin tener que llenar el código de mensajes de impresión.

Los conceptos esenciales de un debugger son:

  • Breakpoint: punto donde se detiene la ejecución.
  • Step over: ejecuta la siguiente línea sin entrar en funciones.
  • Step into: entra en la función que se está ejecutando.
  • Step out: termina la función actual y regresa al punto anterior.
  • Watch: permite observar valores específicos.
  • Call stack: muestra la cadena de llamadas que llevó hasta el punto actual.

Logging

Los logs son fundamentales para investigar problemas, especialmente cuando el programa se ejecuta fuera de un entorno de desarrollo. Ejemplo en Python:

import logging

logging.basicConfig(level=logging.INFO)

def calculate_total(price, quantity):
    logging.info("Calculating total")
    logging.info("price=%s quantity=%s", price, quantity)

    return price * quantity

El logging permite conservar información sobre lo que estaba ocurriendo cuando apareció un problema. En aplicaciones reales conviene registrar información relevante, pero evitar datos sensibles como contraseñas, tokens o información privada.

Excepciones

Las excepciones representan situaciones que impiden continuar una operación normalmente:

try:
    value = int(input("Enter a number: "))
    result = 100 / value
    print(result)

except ValueError:
    print("The input must be a number")

except ZeroDivisionError:
    print("The number cannot be zero")

Aquí cada excepción representa una condición diferente.

Es importante evitar capturar todos los errores sin analizarlos:

try:
    ...
except:
    print("Something went wrong")

Hacer esto puede ocultar problemas importantes. Es preferible capturar excepciones específicas cuando sea posible.

Troubleshooting sistemático

Cuando el problema puede estar en diferentes componentes, conviene dividir el sistema:

Usuario
   |
   v
Frontend
   |
   v
API
   |
   v
Base de datos

Si el usuario no puede obtener información, no debemos asumir inmediatamente que la base de datos es responsable. Podemos comprobar cada capa:

  1. ¿El frontend realiza la petición?
  2. ¿La petición llega a la API?
  3. ¿La API procesa correctamente los datos?
  4. ¿La API puede conectarse a la base de datos?
  5. ¿La base de datos devuelve la información?
  6. ¿La respuesta llega nuevamente al usuario?

Esto reduce el espacio de búsqueda.

Aislar el problema

Aislar significa reducir el sistema hasta encontrar la parte responsable. Supongamos que una función tiene muchas operaciones:

def process_order(order):
    validate(order)
    calculate_price(order)
    apply_discount(order)
    save_order(order)
    send_email(order)

Si sabemos que el pedido se guarda correctamente pero el correo no llega, ya podemos concentrarnos en send_email() y en sus dependencias.

El objetivo es convertir "La aplicación no funciona." en algo específico como: "La función send_email() falla cuando el servidor SMTP rechaza la conexión." Un problema específico es mucho más fácil de resolver.

Cambios recientes

Una pregunta extremadamente útil durante troubleshooting es: ¿qué cambió?

Los problemas frecuentemente aparecen después de una actualización, un cambio de configuración, una nueva dependencia, una modificación del código, un cambio en variables de entorno, una migración de base de datos o un cambio de infraestructura.

El control de versiones ayuda a comparar estados anteriores:

git diff

También permite revisar commits recientes:

git log --oneline

Si el problema comenzó después de un cambio concreto, ese cambio se convierte en una hipótesis fuerte que debe verificarse.

Regresión y pruebas

Después de corregir un error, no basta con comprobar que el caso original funciona. También debemos verificar que la solución no haya roto otra parte del sistema. Por ejemplo:

def divide(a, b):
    if b == 0:
        raise ValueError("b cannot be zero")

    return a / b

Podemos probar:

assert divide(10, 2) == 5

try:
    divide(10, 0)
except ValueError:
    pass
else:
    raise AssertionError("Expected ValueError")

Las pruebas automatizadas ayudan a evitar que el mismo error reaparezca.

Un proceso práctico

Un flujo sencillo de debugging y troubleshooting puede ser:

  1. Definir el comportamiento esperado.
  2. Observar el comportamiento real.
  3. Reproducir el problema.
  4. Recopilar errores, logs y datos.
  5. Formular una hipótesis.
  6. Diseñar una prueba pequeña.
  7. Ejecutar la prueba.
  8. Confirmar o descartar la hipótesis.
  9. Aplicar una corrección.
  10. Ejecutar pruebas.
  11. Verificar que no existan regresiones.
  12. Documentar la causa y la solución.

Este proceso es más efectivo que realizar cambios aleatorios.

Debugging no es adivinar

Un buen programador no necesita conocer inmediatamente la respuesta. Necesita saber cómo obtener evidencia.

La mentalidad correcta es: "¿Qué tendría que observar para confirmar que esta es la causa?" En lugar de: "Creo que esto está fallando, así que voy a cambiarlo."

Esta diferencia parece pequeña, pero cambia completamente la calidad del diagnóstico.

Conclusión

Debugging y troubleshooting son habilidades esenciales porque los sistemas reales inevitablemente presentan errores.

La herramienta más importante no es un debugger específico, un lenguaje de programación o un comando determinado. Es un proceso de razonamiento. Reproducir el problema, recopilar evidencia, formular hipótesis, aislar componentes, medir resultados y verificar las soluciones permite convertir problemas aparentemente complejos en problemas concretos y comprobables.

En resumen:

Observa.
Reproduce.
Mide.
Formula una hipótesis.
Prueba.
Corrige.
Verifica.
Documenta.

Un buen debugging no consiste en encontrar rápidamente cualquier cambio que haga desaparecer el error. Consiste en demostrar por qué el error ocurría y comprobar que la solución realmente corrige la causa.

© Arturo Juárez Monroy