Módulo 05: Manejo de Excepciones

Objetivos del módulo

  • Entender qué son las excepciones y por qué son necesarias
  • Conocer las excepciones predefinidas más comunes de Oracle
  • Capturar y gestionar excepciones en la sección EXCEPTION
  • Definir excepciones personalizadas con RAISE
  • Asociar códigos de error con PRAGMA EXCEPTION_INIT
  • Utilizar SQLCODE y SQLERRM para obtener información del error
  • Lanzar errores de aplicación con RAISE_APPLICATION_ERROR
  • Aplicar buenas prácticas en la gestión de errores

1. ¿Qué es una excepción?

Una excepción es un error o condición anormal que ocurre durante la ejecución de un bloque PL/SQL. Cuando se produce una excepción, el flujo normal del programa se interrumpe y el control pasa a la sección EXCEPTION.

DECLARE
    -- Declaraciones
BEGIN
    -- Código que puede generar errores
EXCEPTION
    -- Aquí se capturan y gestionan los errores
    WHEN excepcion_1 THEN
        -- Tratamiento del error 1
    WHEN excepcion_2 THEN
        -- Tratamiento del error 2
    WHEN OTHERS THEN
        -- Tratamiento para cualquier otro error
END;
/

📘 Concepto: Sin manejo de excepciones, un error detiene la ejecución del bloque y muestra un mensaje críptico al usuario. Con excepciones, puedes capturar el error, informar al usuario de forma amigable, registrar el error en un log o intentar una acción alternativa.


2. Excepciones predefinidas de Oracle

Oracle proporciona un conjunto de excepciones predefinidas que cubren los errores más comunes:

Excepción Código ORA Descripción
NO_DATA_FOUND ORA-01403 SELECT INTO no devuelve ninguna fila
TOO_MANY_ROWS ORA-01422 SELECT INTO devuelve más de una fila
ZERO_DIVIDE ORA-01476 División por cero
VALUE_ERROR ORA-06502 Error de conversión o truncamiento de valor
INVALID_NUMBER ORA-01722 Conversión de texto a número no válida
DUP_VAL_ON_INDEX ORA-00001 Violación de restricción UNIQUE o PRIMARY KEY
CURSOR_ALREADY_OPEN ORA-06511 Intento de abrir un cursor que ya está abierto
INVALID_CURSOR ORA-01001 Operación sobre un cursor no válido (no abierto)
LOGIN_DENIED ORA-01017 Usuario o contraseña incorrectos
TIMEOUT_ON_RESOURCE ORA-00051 Tiempo de espera agotado

2.1. NO_DATA_FOUND

La excepción más común. Se produce cuando SELECT INTO no encuentra ninguna fila:

DECLARE
    v_nombre conductor.nombre%TYPE;
BEGIN
    SELECT nombre INTO v_nombre
    FROM conductor
    WHERE nombre = 'NO EXISTE ESTE CONDUCTOR';
    
    DBMS_OUTPUT.PUT_LINE('Conductor: ' || v_nombre);
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('Error: No se encontró el conductor');
END;
/

2.2. TOO_MANY_ROWS

Se produce cuando SELECT INTO devuelve más de una fila:

DECLARE
    v_nombre conductor.nombre%TYPE;
BEGIN
    -- Esta consulta probablemente devuelve varias filas
    SELECT nombre INTO v_nombre
    FROM conductor;
    
    DBMS_OUTPUT.PUT_LINE('Conductor: ' || v_nombre);
EXCEPTION
    WHEN TOO_MANY_ROWS THEN
        DBMS_OUTPUT.PUT_LINE('Error: La consulta devolvió más de una fila');
        DBMS_OUTPUT.PUT_LINE('Usa un cursor para procesar múltiples filas');
END;
/

2.3. ZERO_DIVIDE

DECLARE
    v_resultado NUMBER;
    v_divisor   NUMBER := 0;
BEGIN
    v_resultado := 100 / v_divisor;
    DBMS_OUTPUT.PUT_LINE('Resultado: ' || v_resultado);
EXCEPTION
    WHEN ZERO_DIVIDE THEN
        DBMS_OUTPUT.PUT_LINE('Error: No se puede dividir por cero');
END;
/

2.4. VALUE_ERROR

Se produce por truncamiento o conversión de valores incorrecta:

DECLARE
    v_numero NUMBER(2);  -- Solo admite 2 dígitos
BEGIN
    v_numero := 999;  -- Demasiado grande
    DBMS_OUTPUT.PUT_LINE('Número: ' || v_numero);
EXCEPTION
    WHEN VALUE_ERROR THEN
        DBMS_OUTPUT.PUT_LINE('Error: Valor demasiado grande o tipo incompatible');
END;
/

2.5. DUP_VAL_ON_INDEX

Se produce al intentar insertar un valor duplicado en una columna con restricción UNIQUE o PRIMARY KEY:

BEGIN
    -- Intentar insertar una matrícula que ya existe
    INSERT INTO vehiculo (matricula, marca, color)
    VALUES ('1234ABC', 'Seat', 'Rojo');
EXCEPTION
    WHEN DUP_VAL_ON_INDEX THEN
        DBMS_OUTPUT.PUT_LINE('Error: Ya existe un vehículo con esa matrícula');
END;
/

3. Capturar múltiples excepciones

Se pueden capturar varias excepciones en el mismo bloque, cada una con su tratamiento:

DECLARE
    v_nombre conductor.nombre%TYPE;
    v_resultado NUMBER;
BEGIN
    SELECT nombre INTO v_nombre
    FROM conductor
    WHERE ROWNUM = 1;
    
    v_resultado := 100 / 0;
    
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('No se encontraron datos');
    WHEN ZERO_DIVIDE THEN
        DBMS_OUTPUT.PUT_LINE('División por cero');
    WHEN TOO_MANY_ROWS THEN
        DBMS_OUTPUT.PUT_LINE('Demasiadas filas');
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error inesperado: ' || SQLERRM);
END;
/

⚠️ Importante: La cláusula WHEN OTHERS captura cualquier excepción no capturada previamente. Debe ir siempre la última. Nunca la uses sola sin registrar o informar del error, ya que podrías ocultar problemas graves.


4. SQLCODE y SQLERRM

Estas funciones proporcionan información detallada sobre el error producido:

Función Descripción
SQLCODE Devuelve el código numérico del error (p.ej. -1403)
SQLERRM Devuelve el mensaje de error completo
DECLARE
    v_nombre conductor.nombre%TYPE;
BEGIN
    SELECT nombre INTO v_nombre
    FROM conductor
    WHERE nombre = 'NO EXISTE';
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Código de error: ' || SQLCODE);
        DBMS_OUTPUT.PUT_LINE('Mensaje: ' || SQLERRM);
END;
/

Resultado:

Código de error: 100
Mensaje: ORA-01403: no data found

Tabla de SQLCODE

Valor Significado
0 No hubo error
+100 NO_DATA_FOUND
Negativo Código de error ORA (p.ej. -1 para DUP_VAL_ON_INDEX)
+1 Excepción definida por el usuario

💡 Truco: Es buena práctica guardar SQLCODE y SQLERRM en variables locales al inicio del bloque EXCEPTION, ya que sus valores pueden cambiar si ejecutas otras sentencias SQL dentro del mismo manejador.

EXCEPTION
    WHEN OTHERS THEN
        DECLARE
            v_code NUMBER := SQLCODE;
            v_msg  VARCHAR2(500) := SQLERRM;
        BEGIN
            DBMS_OUTPUT.PUT_LINE('Error ' || v_code || ': ' || v_msg);
            -- Aquí puedes hacer INSERT para registrar el error en un log
        END;
END;

5. Excepciones definidas por el usuario

Puedes crear tus propias excepciones para controlar situaciones de negocio:

5.1. Declarar y lanzar con RAISE

DECLARE
    e_salario_negativo EXCEPTION;
    v_salario NUMBER := -500;
BEGIN
    IF v_salario < 0 THEN
        RAISE e_salario_negativo;
    END IF;
    
    DBMS_OUTPUT.PUT_LINE('Salario válido: ' || v_salario);
EXCEPTION
    WHEN e_salario_negativo THEN
        DBMS_OUTPUT.PUT_LINE('Error de negocio: El salario no puede ser negativo');
END;
/

5.2. Ejemplo práctico: validación de edad

DECLARE
    e_menor_edad    EXCEPTION;
    e_edad_invalida EXCEPTION;
    v_edad NUMBER := 16;
BEGIN
    IF v_edad < 0 OR v_edad > 150 THEN
        RAISE e_edad_invalida;
    ELSIF v_edad < 18 THEN
        RAISE e_menor_edad;
    END IF;
    
    DBMS_OUTPUT.PUT_LINE('Edad válida: ' || v_edad || ' años');
EXCEPTION
    WHEN e_menor_edad THEN
        DBMS_OUTPUT.PUT_LINE('Error: Debe ser mayor de 18 años (tiene ' || v_edad || ')');
    WHEN e_edad_invalida THEN
        DBMS_OUTPUT.PUT_LINE('Error: La edad no es válida: ' || v_edad);
END;
/

6. PRAGMA EXCEPTION_INIT

PRAGMA EXCEPTION_INIT permite asociar una excepción definida por el usuario con un código de error Oracle específico. Esto es útil para capturar errores Oracle que no tienen una excepción predefinida:

DECLARE
    e_check_violacion EXCEPTION;
    PRAGMA EXCEPTION_INIT(e_check_violacion, -2290);  -- ORA-02290: check constraint violated
BEGIN
    -- Intentar insertar un valor que viola una restricción CHECK
    INSERT INTO infraccion (id, codigo, descripcion, puntos, valor_economico, vigente)
    VALUES (999, 'X', 'Test', -5, 100, 1);  -- puntos negativos si hay CHECK
EXCEPTION
    WHEN e_check_violacion THEN
        DBMS_OUTPUT.PUT_LINE('Error: Se ha violado una restricción CHECK');
        DBMS_OUTPUT.PUT_LINE('Detalle: ' || SQLERRM);
END;
/

Otro ejemplo: FK violada

DECLARE
    e_fk_violada EXCEPTION;
    PRAGMA EXCEPTION_INIT(e_fk_violada, -2291);  -- ORA-02291: integrity constraint violated - parent key not found
BEGIN
    INSERT INTO multa (id, fecha, lugar, id_conductor)
    VALUES (999, SYSDATE, 'Test', 99999);  -- conductor inexistente
EXCEPTION
    WHEN e_fk_violada THEN
        DBMS_OUTPUT.PUT_LINE('Error: El conductor referenciado no existe');
END;
/

📘 Concepto: El pragma EXCEPTION_INIT es una directiva de compilación. Debe ir en la sección DECLARE, justo después de la declaración de la excepción. El número de error siempre es negativo (excepto +100 para NO_DATA_FOUND).


7. RAISE_APPLICATION_ERROR

RAISE_APPLICATION_ERROR permite lanzar un error personalizado con un código y un mensaje que se propaga al programa que llamó al bloque PL/SQL:

RAISE_APPLICATION_ERROR(codigo_error, mensaje [, TRUE/FALSE]);
  • codigo_error: Debe estar entre -20000 y -20999 (rango reservado para aplicaciones)
  • mensaje: Texto descriptivo del error (máx. 2048 caracteres)
  • TRUE/FALSE: Si TRUE, añade el error a la pila de errores existente

7.1. Ejemplo básico

DECLARE
    v_edad NUMBER := 15;
BEGIN
    IF v_edad < 18 THEN
        RAISE_APPLICATION_ERROR(-20001, 
            'El conductor debe tener al menos 18 años. Edad proporcionada: ' || v_edad);
    END IF;
    
    DBMS_OUTPUT.PUT_LINE('Proceso completado');
END;
/

Resultado:

ORA-20001: El conductor debe tener al menos 18 años. Edad proporcionada: 15

7.2. Uso en validaciones de negocio

DECLARE
    v_matricula  VARCHAR2(10) := '1234ABC';
    v_num_multas NUMBER;
    c_max_multas CONSTANT NUMBER := 10;
BEGIN
    SELECT COUNT(*) INTO v_num_multas
    FROM multa m
    JOIN vehiculo v ON m.matricula = v.matricula
    WHERE v.matricula = v_matricula;
    
    IF v_num_multas >= c_max_multas THEN
        RAISE_APPLICATION_ERROR(-20010, 
            'El vehículo ' || v_matricula || 
            ' ha superado el límite de ' || c_max_multas || 
            ' multas (' || v_num_multas || ' registradas)');
    END IF;
    
    DBMS_OUTPUT.PUT_LINE('Vehículo OK. Multas: ' || v_num_multas);
END;
/

⚠️ Importante: RAISE_APPLICATION_ERROR detiene la ejecución del bloque (como cualquier excepción no capturada) y devuelve el error al entorno que llamó al bloque. Es la forma estándar de comunicar errores desde procedimientos y funciones PL/SQL a las aplicaciones cliente.

7.3. Capturar un RAISE_APPLICATION_ERROR

BEGIN
    -- Bloque interno que lanza el error
    BEGIN
        RAISE_APPLICATION_ERROR(-20001, 'Error de prueba');
    EXCEPTION
        WHEN OTHERS THEN
            IF SQLCODE = -20001 THEN
                DBMS_OUTPUT.PUT_LINE('Error de aplicación capturado: ' || SQLERRM);
            ELSE
                RAISE;  -- Re-lanzar si no es el error esperado
            END IF;
    END;
    
    DBMS_OUTPUT.PUT_LINE('El programa continúa después de capturar el error');
END;
/

8. Propagación de excepciones

Cuando una excepción no se captura en un bloque, se propaga al bloque que lo contiene:

DECLARE
    v_nombre conductor.nombre%TYPE;
BEGIN
    DBMS_OUTPUT.PUT_LINE('--- Inicio bloque externo ---');
    
    -- Bloque interno SIN manejador de excepciones
    BEGIN
        SELECT nombre INTO v_nombre
        FROM conductor
        WHERE nombre = 'NO EXISTE';
        
        DBMS_OUTPUT.PUT_LINE('Este mensaje nunca se muestra');
    END;
    
    -- Esta línea NO se ejecuta porque la excepción propagada interrumpe el bloque externo
    DBMS_OUTPUT.PUT_LINE('Este mensaje tampoco se muestra');
    
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('Excepción capturada en el bloque EXTERNO');
END;
/

Re-lanzar una excepción con RAISE

Dentro de un manejador, puedes re-lanzar la misma excepción usando RAISE sin parámetros:

BEGIN
    BEGIN
        INSERT INTO vehiculo (matricula, marca) VALUES (NULL, 'Test');
    EXCEPTION
        WHEN OTHERS THEN
            DBMS_OUTPUT.PUT_LINE('Log: Error en INSERT - ' || SQLERRM);
            RAISE;  -- Re-lanza la excepción al bloque externo
    END;
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error capturado en bloque externo: ' || SQLERRM);
END;
/

💡 Truco: El patrón de capturar → registrar → re-lanzar es muy útil en procedimientos almacenados. Registras el error en una tabla de log y luego re-lanzas para que la aplicación cliente también sea informada.


9. Buenas prácticas en manejo de excepciones

  1. Nunca uses WHEN OTHERS sin registrar el error: capturar todos los errores sin hacer nada es equivalente a ocultarlos.
  2. Sé específico: captura las excepciones concretas que esperas antes de WHEN OTHERS.
  3. Registra los errores: usa una tabla de log para guardar SQLCODE, SQLERRM, la fecha y el contexto.
  4. Usa RAISE_APPLICATION_ERROR para errores de negocio con códigos entre -20000 y -20999.
  5. No abuses de las excepciones para controlar el flujo normal: usa condicionales (IF, CASE) cuando sea posible.
-- Ejemplo de tabla de log de errores
-- CREATE TABLE error_log (
--     id         NUMBER GENERATED ALWAYS AS IDENTITY,
--     fecha      TIMESTAMP DEFAULT SYSTIMESTAMP,
--     codigo     NUMBER,
--     mensaje    VARCHAR2(4000),
--     programa   VARCHAR2(100)
-- );

DECLARE
    v_nombre conductor.nombre%TYPE;
BEGIN
    SELECT nombre INTO v_nombre
    FROM conductor
    WHERE nombre = 'NO EXISTE';
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('Conductor no encontrado');
    WHEN OTHERS THEN
        -- Registrar el error (en un caso real, INSERT en tabla de log)
        DBMS_OUTPUT.PUT_LINE('Error inesperado [' || SQLCODE || ']: ' || SQLERRM);
        RAISE;  -- Re-lanzar para no ocultar el error
END;
/

10. Ejercicios prácticos

Ejercicio 1: Manejo de NO_DATA_FOUND

Busca un conductor por nombre. Si no existe, muestra un mensaje amigable:

DECLARE
    v_nombre conductor.nombre%TYPE;
    v_carnet conductor.fec_carnet%TYPE;
    v_buscar VARCHAR2(50) := 'CONDUCTOR INEXISTENTE';
BEGIN
    SELECT nombre, fec_carnet INTO v_nombre, v_carnet
    FROM conductor
    WHERE UPPER(nombre) = UPPER(v_buscar);
    
    DBMS_OUTPUT.PUT_LINE('Encontrado: ' || v_nombre || ' - Carnet: ' || v_carnet);
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('No se encontró ningún conductor con nombre: ' || v_buscar);
    WHEN TOO_MANY_ROWS THEN
        DBMS_OUTPUT.PUT_LINE('Hay varios conductores con ese nombre. Use un criterio más específico.');
END;
/

Ejercicio 2: Excepción personalizada

Valida que el importe de una infracción esté entre 50 y 5000 euros:

DECLARE
    e_importe_bajo  EXCEPTION;
    e_importe_alto  EXCEPTION;
    v_importe       NUMBER := 25;
BEGIN
    IF v_importe < 50 THEN
        RAISE e_importe_bajo;
    ELSIF v_importe > 5000 THEN
        RAISE e_importe_alto;
    END IF;
    
    DBMS_OUTPUT.PUT_LINE('Importe válido: ' || v_importe || ' €');
EXCEPTION
    WHEN e_importe_bajo THEN
        DBMS_OUTPUT.PUT_LINE('Error: Importe demasiado bajo (' || v_importe || 
                             ' €). Mínimo: 50 €');
    WHEN e_importe_alto THEN
        DBMS_OUTPUT.PUT_LINE('Error: Importe demasiado alto (' || v_importe || 
                             ' €). Máximo: 5000 €');
END;
/

Ejercicio 3: RAISE_APPLICATION_ERROR

Crea una validación que compruebe que un conductor tiene al menos 18 años al sacarse el carnet:

DECLARE
    v_fec_nac DATE := TO_DATE('01/01/2010', 'DD/MM/YYYY');
    v_fec_car DATE := TO_DATE('15/06/2025', 'DD/MM/YYYY');
    v_edad    NUMBER;
BEGIN
    v_edad := TRUNC(MONTHS_BETWEEN(v_fec_car, v_fec_nac) / 12);
    
    IF v_edad < 18 THEN
        RAISE_APPLICATION_ERROR(-20001, 
            'Edad insuficiente para obtener el carnet. Edad: ' || v_edad || ' años');
    END IF;
    
    DBMS_OUTPUT.PUT_LINE('Conductor registrado correctamente. Edad: ' || v_edad);
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
END;
/

Ejercicio 4: PRAGMA EXCEPTION_INIT

Captura el error de clave primaria duplicada al intentar insertar un registro duplicado:

DECLARE
    e_pk_duplicada EXCEPTION;
    PRAGMA EXCEPTION_INIT(e_pk_duplicada, -1);  -- ORA-00001: unique constraint violated
BEGIN
    INSERT INTO infraccion (id, codigo, descripcion, puntos, valor_economico, vigente)
    VALUES (1, 'DUP', 'Infracción duplicada', 0, 0, 0);
    
    DBMS_OUTPUT.PUT_LINE('Registro insertado correctamente');
EXCEPTION
    WHEN e_pk_duplicada THEN
        DBMS_OUTPUT.PUT_LINE('Error: Ya existe un registro con ese ID');
        DBMS_OUTPUT.PUT_LINE('Detalle: ' || SQLERRM);
END;
/

Resumen

Concepto Detalle
Excepción Error o condición anormal durante la ejecución de un bloque PL/SQL
Sección EXCEPTION Bloque donde se capturan y gestionan los errores
NO_DATA_FOUND SELECT INTO no devuelve filas
TOO_MANY_ROWS SELECT INTO devuelve más de una fila
ZERO_DIVIDE División por cero
VALUE_ERROR Truncamiento o conversión incorrecta
DUP_VAL_ON_INDEX Violación de restricción UNIQUE/PK
WHEN OTHERS Captura cualquier excepción no capturada previamente (siempre la última)
SQLCODE Código numérico del error
SQLERRM Mensaje descriptivo del error
RAISE Lanza una excepción definida por el usuario (o re-lanza la actual)
PRAGMA EXCEPTION_INIT Asocia una excepción de usuario con un código ORA específico
RAISE_APPLICATION_ERROR Lanza un error personalizado con código -20000 a -20999
Propagación Excepciones no capturadas se propagan al bloque contenedor