Cómo depurar código: técnicas y herramientas para encontrar errores

D
DanisCh
• 15 min de lectura
Cómo depurar código: técnicas y herramientas para encontrar errores
Consejos para estudiar programación Empezar desde cero

Depurar código es el proceso de encontrar y corregir errores en un programa. Es una habilidad que todo programador usa a diario, y que muchos aprenden de forma intuitiva sin desarrollarla de manera sistemática. El resultado es pasar horas buscando un bug que con la técnica correcta se encontraría en minutos.

En esta guía aprenderás los tipos de errores que existen, las técnicas más efectivas para encontrarlos y las herramientas que los profesionales usan en Python y JavaScript.

Tipos de errores

Antes de aprender a depurar, conviene entender con qué tipo de error estás lidiando. No todos se encuentran de la misma manera.

Errores de sintaxis

El lenguaje no puede interpretar el código porque está mal escrito. Son los más fáciles de encontrar: el intérprete o compilador te dice exactamente en qué línea está el problema antes de ejecutar nada.

# Python: error de sintaxis
def saludar(nombre)    # ← falta los dos puntos
    print(f"Hola, {nombre}")

# SyntaxError: expected ':'

# Más ejemplos de errores de sintaxis:
if edad > 18          # falta los dos puntos
    print("adulto")

print("hola"          # falta el paréntesis de cierre
// JavaScript: errores de sintaxis
function saludar(nombre {   // ← falta el paréntesis de cierre
  console.log(`Hola, ${nombre}`);
}

// SyntaxError: Unexpected token '{'

Errores de ejecución (runtime errors)

El código tiene sintaxis correcta pero algo falla cuando se ejecuta. Se producen en una línea específica y el programa se detiene con un mensaje de error.

# Python: errores de ejecución comunes

# TypeError: operar con tipos incompatibles
edad = "25"
print(edad + 1)   # TypeError: can only concatenate str (not "int") to str

# ZeroDivisionError: dividir entre cero
resultado = 10 / 0   # ZeroDivisionError: division by zero

# IndexError: acceder a un índice que no existe
lista = [1, 2, 3]
print(lista[5])   # IndexError: list index out of range

# KeyError: buscar una clave que no existe en un diccionario
usuario = {"nombre": "Ana"}
print(usuario["email"])   # KeyError: 'email'

# AttributeError: usar un método que no existe
numero = 42
numero.upper()   # AttributeError: 'int' object has no attribute 'upper'

# NameError: usar una variable que no ha sido definida
print(resultado)   # NameError: name 'resultado' is not defined

Errores de lógica

Son los más difíciles de encontrar: el código se ejecuta sin errores pero produce resultados incorrectos. El programa hace algo diferente a lo que el programador tenía en mente.

# Python: errores de lógica

# Calcular el promedio incorrectamente
def promedio(numeros):
    total = 0
    for n in numeros:
        total += n
    return total / len(numeros) + 1   # ← el +1 no debería estar aquí

print(promedio([10, 20, 30]))   # 21.0 en lugar de 20.0
# No hay ningún mensaje de error. El resultado simplemente está mal.

# Condición invertida
def es_adulto(edad):
    if edad < 18:    # ← debería ser >=
        return True
    return False

print(es_adulto(20))   # False ← incorrecto
print(es_adulto(15))   # True  ← incorrecto

Técnica 1: leer el mensaje de error con atención

La habilidad más básica y más subestimada de la depuración. Cuando un programa falla con un error de ejecución, el mensaje contiene exactamente la información que necesitas: el tipo de error, la descripción del problema y el traceback (la pila de llamadas que llevó al error).

# Python: ejemplo de traceback completo
# El traceback se lee de abajo hacia arriba

def calcular_precio(precio, descuento):
    return precio - precio * (descuento / 100)

def procesar_pedido(pedido):
    precio_final = calcular_precio(pedido["precio"], pedido["descuento"])
    return precio_final

pedido = {"precio": 100}  # falta la clave "descuento"
print(procesar_pedido(pedido))
# Traceback que produce:
# Traceback (most recent call last):          ← léelo de abajo hacia arriba
#   File "tienda.py", line 10, in <module>
#     print(procesar_pedido(pedido))           ← aquí se llamó a la función
#   File "tienda.py", line 7, in procesar_pedido
#     precio_final = calcular_precio(...)      ← aquí se llamó a calcular_precio
#   File "tienda.py", line 2, in calcular_precio
#     return precio - precio * (descuento / 100)  ← aquí ocurrió el error
# KeyError: 'descuento'                        ← tipo de error + causa exacta

# La última línea te dice QUÉ salió mal.
# Las líneas del traceback te dicen DÓNDE y CÓMO llegó el programa hasta ahí.

Muchos principiantes ven el traceback y se abruman por su longitud. El truco es ir directamente a la última línea (el tipo de error) y luego buscar la primera línea del traceback que corresponde a tu código, no a una librería externa.

Técnica 2: print debugging

El método más simple y más universalmente usado: añadir instrucciones print() o console.log() para ver el estado del programa en puntos específicos. No es glamuroso, pero funciona y no requiere ninguna herramienta especial.

# Python: print debugging

def buscar_maximo(numeros):
    print(f"[DEBUG] Entrada: {numeros}")   # ver qué recibe la función

    if not numeros:
        print("[DEBUG] Lista vacía, devolviendo None")
        return None

    maximo = numeros[0]
    print(f"[DEBUG] Máximo inicial: {maximo}")

    for i, n in enumerate(numeros):
        print(f"[DEBUG] Comparando {n} con el máximo actual {maximo}")
        if n > maximo:
            maximo = n
            print(f"[DEBUG] Nuevo máximo: {maximo}")

    print(f"[DEBUG] Resultado final: {maximo}")
    return maximo

buscar_maximo([34, 7, 89, 12])
# [DEBUG] Entrada: [34, 7, 89, 12]
# [DEBUG] Máximo inicial: 34
# [DEBUG] Comparando 34 con el máximo actual 34
# [DEBUG] Comparando 7 con el máximo actual 34
# [DEBUG] Comparando 89 con el máximo actual 34
# [DEBUG] Nuevo máximo: 89
# [DEBUG] Comparando 12 con el máximo actual 89
# [DEBUG] Resultado final: 89
# Buenas prácticas para el print debugging:

# 1. Añade una etiqueta clara para distinguir los prints de depuración
print(f"[DEBUG] valor de x: {x}")    # fácil de buscar y eliminar después

# 2. Imprime el tipo además del valor cuando no estés seguro
print(f"[DEBUG] tipo={type(x)}, valor={x}")

# 3. Imprime antes y después de la operación sospechosa
print(f"[DEBUG] antes de calcular: total={total}")
total = calcular_algo(datos)
print(f"[DEBUG] después de calcular: total={total}")

# 4. Usa pprint para estructuras complejas (listas y diccionarios grandes)
from pprint import pprint
pprint(datos_complejos)   # más legible que print() para estructuras anidadas
// JavaScript: console debugging

function procesarUsuarios(usuarios) {
  console.log('[DEBUG] Usuarios recibidos:', usuarios);

  const activos = usuarios.filter(u => u.activo);
  console.log('[DEBUG] Activos:', activos.length);

  const resultado = activos.map(u => {
    console.log('[DEBUG] Procesando:', u.nombre);
    return { ...u, procesado: true };
  });

  console.log('[DEBUG] Resultado final:', resultado);
  return resultado;
}

// Herramientas adicionales de console en JavaScript:
console.table(usuarios);           // muestra arrays de objetos como tabla
console.group('Procesamiento');    // agrupa los logs siguientes
console.log('detalle 1');
console.log('detalle 2');
console.groupEnd();
console.time('operacion');         // medir cuánto tarda algo
// ... código a medir ...
console.timeEnd('operacion');      // imprime el tiempo transcurrido
console.warn('¡Esto es una advertencia!');
console.error('¡Esto es un error!');

Técnica 3: el depurador (debugger)

Un depurador es una herramienta que te permite pausar la ejecución de tu programa en cualquier punto, ver el estado de todas las variables en ese momento, y avanzar el programa instrucción a instrucción. Es mucho más potente que el print debugging para errores complejos.

El depurador de Python en VS Code

VS Code tiene un depurador integrado para Python. Para usarlo:

  1. Abre tu archivo Python en VS Code.
  2. Haz clic en el número de línea donde quieres pausar la ejecución. Aparece un punto rojo: ese es el breakpoint.
  3. Pulsa F5 o ve a Run → Start Debugging.
  4. El programa se ejecuta y se pausa en el breakpoint.

Una vez pausado, tienes estas acciones:

  • Step Over (F10): ejecuta la línea actual y pasa a la siguiente, sin entrar en funciones.
  • Step Into (F11): ejecuta la línea actual y entra dentro de la función que se llama.
  • Step Out (Shift+F11): sale de la función actual y vuelve al código que la llamó.
  • Continue (F5): continúa la ejecución hasta el siguiente breakpoint.

En el panel de Variables (izquierda) ves el valor de todas las variables locales y globales en ese momento. Puedes pasar el cursor por encima de cualquier variable en el código para ver su valor actual.

pdb: el depurador de Python en la terminal

# Insertar un breakpoint directamente en el código
def calcular_descuento(precio, porcentaje):
    breakpoint()   # la ejecución se pausa aquí (Python 3.7+)
    descuento = precio * porcentaje / 100
    return precio - descuento

calcular_descuento(100, 20)
# Cuando el programa llega a breakpoint(), se abre el prompt de pdb:
# > archivo.py(3)calcular_descuento()
# -> descuento = precio * porcentaje / 100
# (Pdb)

# Comandos de pdb más útiles:
# n (next):         ejecutar la línea actual y pasar a la siguiente
# s (step):         entrar en la función que se llama
# c (continue):     continuar hasta el siguiente breakpoint
# p expresion:      imprimir el valor de una expresión
# pp expresion:     imprimir con pprint (más legible)
# l (list):         mostrar el código alrededor de la línea actual
# q (quit):         salir del depurador
# h (help):         mostrar ayuda
# where:            mostrar la pila de llamadas actual

# En el prompt de pdb puedes ejecutar cualquier expresión Python:
# (Pdb) p precio         → 100
# (Pdb) p porcentaje     → 20
# (Pdb) p precio * porcentaje / 100   → 20.0

El depurador en el navegador (JavaScript)

// Insertar un breakpoint en JavaScript
function calcularDescuento(precio, porcentaje) {
  debugger;   // el navegador pausa la ejecución aquí si DevTools está abierto
  const descuento = precio * porcentaje / 100;
  return precio - descuento;
}

Para usar el depurador del navegador:

  1. Abre Chrome DevTools con F12.
  2. Ve a la pestaña Sources.
  3. Encuentra tu archivo JavaScript y haz clic en el número de línea para añadir un breakpoint.
  4. Recarga la página o ejecuta la acción que llama a ese código.
  5. El navegador pausa la ejecución en el breakpoint y puedes usar los controles de Step Over, Step Into, etc.

Técnica 4: reducir el problema

Cuando tienes un bug en un programa grande, el primer paso es aislar el problema: encontrar el mínimo código que reproduce el error. Esto tiene dos ventajas: te ayuda a entender el problema y hace mucho más fácil buscar ayuda online o pedir ayuda a alguien.

# Supongamos que tienes 200 líneas de código y un bug en algún lugar.
# Estrategia de búsqueda binaria:

# 1. Pon un print/breakpoint en el punto medio del código
# 2. ¿El error ocurre antes o después de ese punto?
# 3. Descarta la mitad que no tiene el bug
# 4. Repite con la mitad restante

# En pocos pasos localizas la zona exacta, aunque el código sea muy largo.

# Ejemplo: proceso de pago que falla en algún punto
def procesar_pago(pedido):
    pedido = validar_pedido(pedido)
    # ← ¿el error es antes o después de aquí? → print(pedido)
    pedido = calcular_impuestos(pedido)
    # ← ¿el error es antes o después de aquí? → print(pedido)
    resultado = cobrar_tarjeta(pedido)
    # ← ¿el error es antes o después de aquí? → print(resultado)
    enviar_confirmacion(resultado)
    return resultado

Técnica 5: el método del pato de goma

Explica tu código en voz alta, línea a línea, a un objeto inanimado (un pato de goma, una planta, una taza). Suena ridículo pero funciona. El proceso de verbalizar lo que hace el código te obliga a ser explícito sobre cada suposición que estás haciendo, y a menudo ves el error en el momento en que lo describes.

"Esta función recibe una lista de usuarios... itera sobre cada uno... ah, espera, aquí estoy asumiendo que todos tienen el campo 'email' pero los usuarios sin cuenta no lo tienen."

Si no tienes un pato a mano, escribe la explicación. El efecto es el mismo.

Técnica 6: leer el código hacia atrás

Cuando tienes un resultado incorrecto, parte del resultado y trabaja hacia atrás. En lugar de seguir el flujo del código de principio a fin, pregúntate: "¿Qué operación produjo este valor? ¿Qué operación produjo el valor que entró a esa operación?" y así sucesivamente hasta encontrar dónde el valor empezó a ser incorrecto.

# El total final es 0 pero debería ser 150. ¿Por qué?
total = calcular_total(pedido)   # → 0

# ¿Por qué es 0? Veamos calcular_total...
def calcular_total(pedido):
    subtotal = sum(p["precio"] for p in pedido["productos"])  # → 150
    descuento = calcular_descuento(subtotal, pedido["descuento"])  # → ?
    return subtotal - descuento  # → 0, lo que significa que descuento = 150

# ¿Por qué descuento es 150? Veamos calcular_descuento...
def calcular_descuento(subtotal, porcentaje):
    return subtotal * porcentaje  # ← AHÍ ESTÁ: falta dividir entre 100
    # debería ser: return subtotal * porcentaje / 100

Herramientas de depuración en Python

# 1. logging: mejor que print() para proyectos reales
import logging

# Configurar el nivel de logging
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def calcular_precio(precio, descuento):
    logging.debug(f"Calculando precio: precio={precio}, descuento={descuento}")

    if descuento < 0 or descuento > 100:
        logging.error(f"Descuento inválido: {descuento}")
        raise ValueError(f"El descuento debe estar entre 0 y 100")

    resultado = precio * (1 - descuento / 100)
    logging.info(f"Precio calculado: {resultado}")
    return resultado

# Ventajas sobre print():
# - Niveles de severidad: DEBUG, INFO, WARNING, ERROR, CRITICAL
# - Puedes activar/desactivar sin borrar el código
# - Puedes escribir a archivo en producción
# - Incluye fecha/hora automáticamente

# En producción, cambias el nivel a WARNING y solo ves los errores importantes
logging.basicConfig(level=logging.WARNING)
# 2. assert: verificar suposiciones en el código
def calcular_promedio(numeros):
    assert len(numeros) > 0, "La lista no puede estar vacía"
    assert all(isinstance(n, (int, float)) for n in numeros), "Todos deben ser números"

    return sum(numeros) / len(numeros)

# Si la suposición es falsa, AssertionError te dice exactamente qué esperabas:
calcular_promedio([])
# AssertionError: La lista no puede estar vacía

calcular_promedio([1, 2, "tres"])
# AssertionError: Todos deben ser números
# 3. traceback: obtener información detallada de excepciones
import traceback

try:
    resultado = 10 / 0
except ZeroDivisionError:
    # Imprimir el traceback completo aunque estés en un except
    traceback.print_exc()

    # O capturarlo como string
    error_detallado = traceback.format_exc()
    logging.error(f"Error inesperado:\n{error_detallado}")

Errores comunes y cómo encontrarlos rápido

Off-by-one error: el error de "uno de más o uno de menos"

# Muy común en bucles y con índices
# Imprimir los primeros 5 elementos de una lista
numeros = [10, 20, 30, 40, 50, 60, 70]

# ❌ Se imprime el índice 5 también (6 elementos, no 5)
for i in range(0, 6):
    print(numeros[i])

# ✅ range(0, 5) genera: 0, 1, 2, 3, 4
for i in range(0, 5):
    print(numeros[i])

# O más idiomático en Python:
for n in numeros[:5]:
    print(n)

Variable no inicializada o con valor incorrecto

# El acumulador se reinicializa dentro del bucle (error clásico)
numeros = [1, 2, 3, 4, 5]

# ❌ total se reinicia en cada iteración
for n in numeros:
    total = 0     # ← esto está mal: debería estar FUERA del bucle
    total += n
print(total)   # 5 (solo el último)

# ✅ total se inicializa una vez antes del bucle
total = 0
for n in numeros:
    total += n
print(total)   # 15

Modificar una lista mientras la recorres

# ❌ Modificar la lista que estás iterando causa comportamiento inesperado
numeros = [1, 2, 3, 4, 5, 6]
for n in numeros:
    if n % 2 == 0:
        numeros.remove(n)   # ← peligroso: se saltan elementos
print(numeros)   # [1, 3, 5] en Python... a veces. Comportamiento indefinido.

# ✅ Crear una nueva lista con los elementos que quieres conservar
numeros = [1, 2, 3, 4, 5, 6]
impares = [n for n in numeros if n % 2 != 0]
print(impares)   # [1, 3, 5] siempre correcto

Confundir == con is en Python

# == compara el VALOR
# is compara la IDENTIDAD (si son el mismo objeto en memoria)

lista1 = [1, 2, 3]
lista2 = [1, 2, 3]

print(lista1 == lista2)   # True  ← mismo valor
print(lista1 is lista2)   # False ← no son el mismo objeto en memoria

# El error clásico: usar 'is' para comparar strings o números
nombre = "ana"
if nombre is "ana":   # ← puede funcionar a veces por optimización interna de Python
    print("correcto")  #    pero NO es fiable. Usa == para comparar valores.

# Cuándo usar 'is':
# Solo para comparar con None, True y False
if resultado is None:   # ✅ correcto
    print("sin resultado")

if resultado == None:   # ❌ funciona pero no es la forma idiomática en Python
    print("sin resultado")

El proceso de depuración: paso a paso

Cuando te encuentras un bug, seguir este proceso evita perder tiempo dando vueltas:

  1. Reproduce el error de forma consistente. Asegúrate de que puedes provocar el error a voluntad. Un error que no se reproduce de forma fiable es imposible de depurar.
  2. Lee el mensaje de error completo. No lo ignores por miedo. La última línea te dice qué falló. El traceback te dice dónde.
  3. Entiende qué debería ocurrir. Antes de buscar el error, tenlo claro: ¿qué resultado esperas? ¿Qué hace el programa en su lugar?
  4. Localiza la zona del código. Usa print debugging o el depurador para encontrar en qué función o bloque empieza el comportamiento incorrecto.
  5. Formula una hipótesis. "Creo que el error está aquí porque esta variable tiene el valor X cuando debería tener Y."
  6. Verifica la hipótesis. Añade un print o breakpoint para confirmar si tu hipótesis es correcta.
  7. Corrige el error y verifica la corrección. Una vez corregido, ejecuta el caso que producía el error y también otros casos para asegurarte de no haber roto algo más.

Resumen

  • Hay tres tipos de errores: sintaxis (el código está mal escrito), ejecución (falla al correr) y lógica (funciona pero da resultados incorrectos). Los de lógica son los más difíciles.
  • Lee el traceback de abajo hacia arriba: la última línea dice qué falló, las anteriores muestran cómo llegó el programa hasta ahí.
  • El print debugging es la técnica más rápida para problemas simples: añade prints con etiquetas claras y elimínalos cuando termines.
  • El depurador (VS Code, pdb, Chrome DevTools) es la herramienta correcta para problemas complejos: pausa la ejecución, inspecciona variables y avanza instrucción a instrucción.
  • Para localizar un bug en código largo, usa búsqueda binaria: descarta la mitad del código en cada paso.
  • El método del pato de goma funciona: explica el código en voz alta y a menudo encuentras el error al verbalizarlo.
  • Usa logging en lugar de print() en proyectos reales: puedes activar y desactivar los mensajes sin borrar código y añade información de contexto automáticamente.

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

1 COMENTARIOS
K
Kal An Zans
2 months ago
Excelente artículo, felicidades. Sin embargo, al intentar acceder desde Brasil —mi país—, aparece el mensaje: «Su país no tiene permiso para acceder a este recurso».
Escribe tu opinión
Respondiendo a