Qué son las excepciones y cómo manejar errores en tu código

D
DanisCh
(Actualizado: ) 14 min de lectura
Qué son las excepciones y cómo manejar errores en tu código
Empezar desde cero Python

Todo programa que interactúa con el mundo real encuentra situaciones inesperadas: un archivo que no existe, una conexión de red que falla, un usuario que introduce texto donde se esperaba un número, una base de datos que no responde. Las excepciones son el mecanismo que tienen los lenguajes de programación para gestionar estas situaciones de forma controlada, sin que el programa se caiga de forma abrupta.

En esta guía aprenderás qué son las excepciones, cómo capturarlas, cómo lanzarlas tú mismo y cómo diseñar una estrategia de manejo de errores que haga tu código más robusto.

Qué es una excepción

Cuando un programa encuentra una situación que no puede gestionar normalmente, lanza una excepción: una señal de que algo salió mal. Si esa excepción no se captura en ningún lugar, el programa se detiene y muestra un mensaje de error.

# Python: excepción no capturada
numeros = [10, 20, 30]
print(numeros[5])

# El programa se detiene con:
# IndexError: list index out of range
#
# La ejecución se interrumpe completamente aquí.
# Cualquier código después de esta línea no se ejecuta.

El manejo de excepciones permite que el programa responda de forma inteligente a esos errores en lugar de simplemente caerse:

# Python: excepción capturada
numeros = [10, 20, 30]
try:
    print(numeros[5])
except IndexError:
    print("Ese índice no existe en la lista")

# El programa continúa ejecutándose normalmente
print("El programa sigue funcionando")

Try / Except / Else / Finally en Python

Python tiene cuatro bloques para gestionar excepciones. Cada uno tiene un propósito específico:

# Estructura completa
try:
    # Código que puede fallar
    resultado = operacion_riesgosa()
except TipoDeError:
    # Se ejecuta si ocurre ese tipo de error en el try
    manejar_el_error()
else:
    # Se ejecuta SOLO si el try terminó SIN excepciones
    usar_resultado(resultado)
finally:
    # Se ejecuta SIEMPRE, haya error o no
    limpiar_recursos()
# Ejemplo real: leer un archivo
def leer_configuracion(ruta):
    archivo = None
    try:
        archivo = open(ruta, "r")
        contenido = archivo.read()
        datos = json.loads(contenido)
    except FileNotFoundError:
        print(f"El archivo {ruta} no existe")
        return None
    except json.JSONDecodeError as e:
        print(f"El archivo no tiene formato JSON válido: {e}")
        return None
    else:
        # Solo llega aquí si todo salió bien
        print(f"Configuración cargada: {len(datos)} parámetros")
        return datos
    finally:
        # Siempre cerramos el archivo, haya error o no
        if archivo:
            archivo.close()

El bloque else: por qué existe y cuándo usarlo

El bloque else se ejecuta solo cuando el bloque try terminó sin excepciones. Es diferente a poner el código al final del try: el código en else no está protegido por el except. Esto es útil cuando quieres separar el código que puede fallar del código que procesa el resultado exitoso.

import json

def cargar_datos(ruta):
    try:
        with open(ruta) as f:
            datos = json.load(f)
    except FileNotFoundError:
        return None
    else:
        # Este código se ejecuta solo si open() y json.load() tuvieron éxito
        # Si procesar_datos() lanza una excepción, NO la captura el except de arriba
        return procesar_datos(datos)

El bloque finally: limpieza garantizada

finally se ejecuta siempre, incluso si hay una excepción no capturada, incluso si hay un return dentro del try o del except. Es el lugar correcto para liberar recursos: cerrar archivos, cerrar conexiones de base de datos, liberar locks.

import psycopg2

def consultar_base_datos(sql):
    conn = psycopg2.connect("postgresql://...")
    try:
        cur = conn.cursor()
        cur.execute(sql)
        return cur.fetchall()
    except psycopg2.Error as e:
        conn.rollback()
        print(f"Error en la consulta: {e}")
        return None
    finally:
        conn.close()   # se ejecuta SIEMPRE, garantizado
        # Si no usamos finally y hay un error, la conexión quedaría abierta

Capturar múltiples tipos de excepciones

# Forma 1: varios bloques except (para tratarlos diferente)
def convertir_a_entero(texto):
    try:
        return int(texto)
    except ValueError:
        print(f"'{texto}' no es un número válido")
        return None
    except TypeError:
        print("El argumento debe ser un string o número")
        return None

print(convertir_a_entero("42"))     # 42
print(convertir_a_entero("hola"))   # 'hola' no es un número válido → None
print(convertir_a_entero(None))     # El argumento debe ser... → None


# Forma 2: capturar varios en el mismo except (para tratarlos igual)
def procesar_entrada(valor):
    try:
        resultado = int(valor) * 2
        return resultado
    except (ValueError, TypeError):
        print("Entrada inválida")
        return 0


# Forma 3: capturar la excepción como variable para ver el mensaje
def dividir(a, b):
    try:
        return a / b
    except ZeroDivisionError as e:
        print(f"Error: {e}")   # imprime "division by zero"
        return None
    except TypeError as e:
        print(f"Tipos incorrectos: {e}")
        return None

Capturar Exception: cuándo y por qué con cuidado

# Exception es la clase base de casi todas las excepciones
# Capturarla coge "todo lo que puede pasar" (excepto errores del sistema como KeyboardInterrupt)

# ❌ Trampa común: capturar Exception sin información
def hacer_algo():
    try:
        resultado = operacion_compleja()
    except Exception:
        pass   # silenciar el error → muy mala práctica
        # El programa continúa como si nada, con un estado potencialmente corrupto

# ✅ Si capturas Exception, al menos registra el error
import logging

def hacer_algo():
    try:
        resultado = operacion_compleja()
        return resultado
    except Exception as e:
        logging.error(f"Error inesperado: {e}", exc_info=True)
        # exc_info=True incluye el traceback completo en el log
        return None

# ✅ Mejor aún: captura solo las excepciones que esperas y sabes manejar
def hacer_algo():
    try:
        resultado = operacion_compleja()
        return resultado
    except ValueError as e:
        logging.warning(f"Valor inválido: {e}")
        return valor_por_defecto
    except ConnectionError as e:
        logging.error(f"Sin conexión: {e}")
        raise   # volver a lanzar para que el nivel superior lo gestione

Lanzar excepciones con raise

No solo capturas excepciones: también puedes lanzarlas tú mismo cuando tu código detecta una condición inválida. Esto hace que tus funciones comuniquen explícitamente qué está mal y dónde.

# Lanzar una excepción existente
def calcular_raiz_cuadrada(numero):
    if numero < 0:
        raise ValueError(f"No se puede calcular la raíz cuadrada de {numero}: número negativo")
    return numero ** 0.5

# El código que llama a la función recibe una excepción clara
try:
    resultado = calcular_raiz_cuadrada(-4)
except ValueError as e:
    print(e)   # "No se puede calcular la raíz cuadrada de -4: número negativo"
# Relanzar una excepción: capturar, hacer algo, y volver a lanzar
def obtener_usuario(usuario_id):
    try:
        return base_de_datos.buscar(usuario_id)
    except DatabaseError as e:
        logging.error(f"Error al buscar usuario {usuario_id}: {e}")
        raise   # relanza la MISMA excepción con su traceback original

# raise sin argumentos relanza la excepción que está siendo manejada
# El traceback original se conserva, lo que facilita la depuración
# Encadenar excepciones: lanzar una nueva que indica la causa original
def cargar_configuracion(ruta):
    try:
        with open(ruta) as f:
            return json.load(f)
    except FileNotFoundError as e:
        raise RuntimeError(f"No se pudo iniciar la aplicación: falta {ruta}") from e
        # "from e" preserva la excepción original como causa
        # El traceback mostrará ambas excepciones y la relación entre ellas

# El traceback mostrará:
# FileNotFoundError: [Errno 2] No such file or directory: 'config.json'
#
# The above exception was the direct cause of the following exception:
#
# RuntimeError: No se pudo iniciar la aplicación: falta config.json

Crear excepciones personalizadas

Cuando construyes una librería o una aplicación con cierta complejidad, crear tus propias clases de excepción hace que el código sea más claro y que los errores sean más fáciles de identificar y manejar.

# Jerarquía de excepciones personalizadas
class ErrorAplicacion(Exception):
    """Excepción base para todos los errores de la aplicación."""
    pass

class ErrorValidacion(ErrorAplicacion):
    """Error de validación de datos de entrada."""
    def __init__(self, campo, mensaje):
        self.campo   = campo
        self.mensaje = mensaje
        super().__init__(f"Error de validación en '{campo}': {mensaje}")

class ErrorAutenticacion(ErrorAplicacion):
    """Error de autenticación o autorización."""
    pass

class ErrorRecursoNoEncontrado(ErrorAplicacion):
    """Un recurso solicitado no existe."""
    def __init__(self, tipo_recurso, identificador):
        self.tipo_recurso  = tipo_recurso
        self.identificador = identificador
        super().__init__(f"{tipo_recurso} con id '{identificador}' no encontrado")

class ErrorServicioExterno(ErrorAplicacion):
    """Error al comunicarse con un servicio externo."""
    def __init__(self, servicio, mensaje_original):
        self.servicio = servicio
        super().__init__(f"Error en {servicio}: {mensaje_original}")
# Usando las excepciones personalizadas
def registrar_usuario(email, edad, contraseña):
    # Validaciones con excepciones descriptivas
    if not "@" in email:
        raise ErrorValidacion("email", "no tiene el formato correcto")

    if edad < 0 or edad > 150:
        raise ErrorValidacion("edad", f"el valor {edad} no es una edad válida")

    if len(contraseña) < 8:
        raise ErrorValidacion("contraseña", "debe tener al menos 8 caracteres")

    # Comprobaciones de negocio
    if usuario_existe(email):
        raise ErrorValidacion("email", "ya está registrado")

    return crear_usuario(email, edad, contraseña)


def obtener_usuario(usuario_id):
    usuario = base_de_datos.buscar(usuario_id)
    if not usuario:
        raise ErrorRecursoNoEncontrado("Usuario", usuario_id)
    return usuario


# El código que usa estas funciones puede manejar cada caso específicamente
def endpoint_registro(datos):
    try:
        usuario = registrar_usuario(datos["email"], datos["edad"], datos["contraseña"])
        return {"status": "ok", "id": usuario.id}
    except ErrorValidacion as e:
        return {"status": "error", "campo": e.campo, "mensaje": e.mensaje}, 400
    except ErrorAplicacion as e:
        return {"status": "error", "mensaje": str(e)}, 500

Manejo de excepciones en JavaScript

// JavaScript: try / catch / finally
function dividir(a, b) {
  try {
    if (b === 0) {
      throw new Error('No se puede dividir entre cero');
    }
    return a / b;
  } catch (error) {
    console.error('Error al dividir:', error.message);
    return null;
  } finally {
    console.log('Operación de división completada');
  }
}

console.log(dividir(10, 2));    // 5
console.log(dividir(10, 0));    // null (con mensaje de error)
// Tipos de errores en JavaScript
try {
  null.propiedad;
} catch (e) {
  console.log(e instanceof TypeError);    // true
  console.log(e.name);                    // "TypeError"
  console.log(e.message);                 // "Cannot read properties of null"
}

try {
  variableNoDefinida;
} catch (e) {
  console.log(e instanceof ReferenceError);  // true
}

try {
  eval('{');
} catch (e) {
  console.log(e instanceof SyntaxError);  // true
}
// Crear clases de error personalizadas en JavaScript
class ErrorValidacion extends Error {
  constructor(campo, mensaje) {
    super(`Error de validación en '${campo}': ${mensaje}`);
    this.name   = 'ErrorValidacion';
    this.campo  = campo;
  }
}

class ErrorRecursoNoEncontrado extends Error {
  constructor(recurso, id) {
    super(`${recurso} con id '${id}' no encontrado`);
    this.name   = 'ErrorRecursoNoEncontrado';
    this.recurso = recurso;
    this.id      = id;
  }
}

// Uso
function obtenerUsuario(id) {
  const usuario = baseDeDatos.buscar(id);
  if (!usuario) {
    throw new ErrorRecursoNoEncontrado('Usuario', id);
  }
  return usuario;
}

try {
  const usuario = obtenerUsuario(999);
} catch (e) {
  if (e instanceof ErrorRecursoNoEncontrado) {
    console.log(`No se encontró: ${e.recurso} (id: ${e.id})`);
  } else {
    throw e;   // relanzar errores que no sabemos manejar
  }
}

Manejo de errores en funciones asíncronas

// JavaScript: manejo de errores en código asíncrono

// Con async/await: try/catch funciona igual que en código síncrono
async function obtenerDatos(url) {
  try {
    const respuesta = await fetch(url);

    if (!respuesta.ok) {
      throw new Error(`HTTP ${respuesta.status}: ${respuesta.statusText}`);
    }

    const datos = await respuesta.json();
    return datos;
  } catch (error) {
    if (error instanceof TypeError) {
      // TypeError ocurre cuando fetch no puede conectarse (sin red, URL inválida)
      console.error('Error de red:', error.message);
    } else {
      console.error('Error al obtener datos:', error.message);
    }
    return null;
  }
}

// Con promesas: usar .catch()
fetch('https://api.ejemplo.com/datos')
  .then(respuesta => {
    if (!respuesta.ok) throw new Error(`HTTP ${respuesta.status}`);
    return respuesta.json();
  })
  .then(datos => procesar(datos))
  .catch(error => console.error('Error:', error.message));

El gestor de contexto: la forma correcta de manejar recursos en Python

Python tiene una forma más elegante de garantizar la limpieza de recursos que el bloque finally: el gestor de contexto con with. Es el equivalente a try/finally pero más conciso y menos propenso a errores.

# Sin gestor de contexto (más verboso y más fácil olvidar cerrar)
archivo = open("datos.txt", "r")
try:
    contenido = archivo.read()
finally:
    archivo.close()   # se ejecuta siempre, pero es fácil olvidarlo

# Con gestor de contexto (forma correcta)
with open("datos.txt", "r") as archivo:
    contenido = archivo.read()
# El archivo se cierra automáticamente al salir del bloque with,
# haya o no una excepción

# Múltiples recursos en un solo with
with open("entrada.txt") as entrada, open("salida.txt", "w") as salida:
    for linea in entrada:
        salida.write(linea.upper())
# Ambos archivos se cierran automáticamente
# Crear tu propio gestor de contexto
from contextlib import contextmanager
import psycopg2

@contextmanager
def conexion_bd(url):
    conn = psycopg2.connect(url)
    try:
        yield conn    # el código del bloque 'with' se ejecuta aquí
        conn.commit() # si no hubo excepciones, confirmar la transacción
    except Exception:
        conn.rollback()  # si hubo error, revertir
        raise
    finally:
        conn.close()     # siempre cerrar la conexión

# Uso elegante
with conexion_bd("postgresql://...") as conn:
    conn.execute("INSERT INTO usuarios (nombre) VALUES (%s)", ["Ana"])
# La transacción se confirma y la conexión se cierra automáticamente

Buenas prácticas en el manejo de excepciones

Captura solo las excepciones que sabes manejar. Si capturas una excepción y no sabes qué hacer con ella, lo más honesto es dejarla propagarse para que alguien más arriba en la cadena de llamadas la gestione. Capturar y silenciar excepciones que no entiendes produce bugs muy difíciles de encontrar.

No uses excepciones para el flujo normal del programa. Las excepciones son para situaciones excepcionales, no para controlar el flujo normal. Si usas excepciones para casos que esperas que ocurran regularmente, el código es más lento y más difícil de leer.

# ❌ Usar excepciones para flujo normal
def obtener_valor(diccionario, clave):
    try:
        return diccionario[clave]
    except KeyError:
        return None   # esto es un caso normal, no una excepción

# ✅ Usar el método pensado para eso
def obtener_valor(diccionario, clave):
    return diccionario.get(clave)   # get() devuelve None si no existe la clave

Los mensajes de error deben ser útiles. Un mensaje de error bueno explica qué salió mal, por qué y, si es posible, cómo se puede corregir.

# ❌ Mensaje de error inútil
raise ValueError("Error en los datos")

# ✅ Mensaje que ayuda a diagnosticar el problema
raise ValueError(
    f"El campo 'edad' tiene el valor '{edad}', que no es válido. "
    f"Debe ser un número entero entre 0 y 150."
)

Registra los errores antes de silenciarlos. Si en algún punto decides capturar una excepción y continuar, asegúrate de registrarla primero. Los errores silenciosos son los más difíciles de diagnosticar en producción.

import logging

# ❌ Silenciar sin registrar
try:
    enviar_email(usuario)
except Exception:
    pass   # si falla, nadie se entera

# ✅ Registrar antes de continuar
try:
    enviar_email(usuario)
except Exception as e:
    logging.error(f"No se pudo enviar email a {usuario.email}: {e}", exc_info=True)
    # exc_info=True incluye el traceback completo en el log

Resumen

  • Una excepción es una señal de error que interrumpe la ejecución normal del programa. Si no se captura, el programa se detiene.
  • El bloque try/except permite capturar excepciones y responder de forma controlada. else se ejecuta solo si no hubo errores; finally se ejecuta siempre.
  • Captura solo las excepciones que sabes manejar. Evita capturar Exception de forma genérica sin registrar el error.
  • Usa raise para lanzar excepciones cuando tu código detecta una condición inválida. Los mensajes deben ser descriptivos y útiles.
  • Crea clases de excepción personalizadas para representar los errores específicos de tu aplicación. Facilitan el manejo preciso de cada caso.
  • En Python, usa el gestor de contexto with para manejar recursos (archivos, conexiones) de forma segura y concisa.
  • En JavaScript, try/catch funciona igual para código síncrono y asíncrono con async/await. Para promesas, usa .catch().
  • Los errores silenciados sin registro son el peor tipo de error: el programa parece funcionar pero algo está mal y nunca lo sabrás.
Etiquetas: Python

¿Te ha gustado esta entrada?

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

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a