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
SQLCODEySQLERRMpara 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 OTHERScaptura 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
SQLCODEySQLERRMen variables locales al inicio del bloqueEXCEPTION, 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_INITes una directiva de compilación. Debe ir en la secciónDECLARE, justo después de la declaración de la excepción. El número de error siempre es negativo (excepto +100 paraNO_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_ERRORdetiene 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
- Nunca uses
WHEN OTHERSsin registrar el error: capturar todos los errores sin hacer nada es equivalente a ocultarlos. - Sé específico: captura las excepciones concretas que esperas antes de
WHEN OTHERS. - Registra los errores: usa una tabla de log para guardar
SQLCODE,SQLERRM, la fecha y el contexto. - Usa
RAISE_APPLICATION_ERRORpara errores de negocio con códigos entre -20000 y -20999. - 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 |