DECLARE HANDLER
DECLARE HANDLER define qué debe hacer MySQL cuando ocurre una condición concreta durante la ejecución de un procedimiento almacenado, una función o un trigger. Es el mecanismo central de manejo de errores del lenguaje almacenado de MySQL y cumple el mismo papel que el bloque try-catch de lenguajes como Java o el try-except de Python: te permite anticiparte a un fallo y decidir cómo reaccionar en lugar de dejar que el error aborte sin más toda la operación.
La idea es declarar, al principio de un bloque, uno o varios handlers que quedan "a la espera". Cuando durante la ejecución surge una de las condiciones vigiladas, MySQL transfiere el control al handler correspondiente, ejecuta su lógica y luego, según el tipo de handler, continúa donde estaba o sale del bloque. A lo largo de este artículo veremos los dos tipos de handler que existen, qué condiciones se pueden vigilar, cómo capturar errores muy específicos y cómo combinar varios handlers en un mismo procedimiento.
Sintaxis
La estructura de una declaración de handler consta de tres partes: el tipo de handler, la condición o condiciones que activan el handler, y la sentencia que se ejecuta cuando se dispara. Esa sentencia puede ser una única instrucción o un bloque BEGIN ... END completo.
DECLARE tipo_handler HANDLER
FOR condicion [, condicion ...]
sentencia;Todos los handlers deben declararse después de las variables y las condiciones, pero antes del resto de la lógica del bloque. Si los colocas en otro sitio, MySQL devuelve un error de sintaxis.
Tipos de handler
Existen dos tipos de handler, y la diferencia entre ellos está en qué ocurre después de que su lógica termina. Esta elección es probablemente la decisión más importante al manejar un error, así que conviene tenerla clara.
| Tipo | Comportamiento |
|---|---|
| CONTINUE | Ejecuta la sentencia del handler y continúa la ejecución |
| EXIT | Ejecuta la sentencia del handler y sale del bloque BEGIN...END actual |
En resumen, CONTINUE se usa cuando el error no es fatal y quieres seguir adelante (por ejemplo, marcar una bandera y proseguir), mientras que EXIT se usa cuando el error invalida el resto del bloque y lo mejor es abandonarlo de forma ordenada.
Condiciones que se pueden manejar
Un handler puede reaccionar a distintos tipos de condición, desde un código de error muy concreto hasta categorías generales que agrupan muchos errores. La siguiente tabla recoge las opciones disponibles.
| Condición | Descripción |
|---|---|
| SQLSTATE 'valor' | Un código SQLSTATE específico |
| mysql_error_code | Un código de error numérico de MySQL |
| SQLWARNING | Cualquier SQLSTATE que empiece con '01' |
| NOT FOUND | SQLSTATE '02000', usado con cursores |
| SQLEXCEPTION | Cualquier SQLSTATE que no empiece con '00', '01' o '02' |
| nombre_condicion | Una condición definida con DECLARE CONDITION |
Las tres categorías generales (SQLEXCEPTION, SQLWARNING y NOT FOUND) son las que más se usan en la práctica, porque permiten capturar familias enteras de problemas con una sola declaración.
CONTINUE HANDLER
Empecemos por el tipo más habitual. En el siguiente procedimiento declaramos un CONTINUE HANDLER para SQLEXCEPTION que, ante cualquier excepción, se limita a poner una variable booleana a TRUE. Como es de tipo CONTINUE, la ejecución prosigue después del INSERT que falla, y más adelante comprobamos esa bandera para decidir qué mensaje devolver.
DELIMITER //
CREATE PROCEDURE insertar_con_handler()
BEGIN
DECLARE v_error BOOLEAN DEFAULT FALSE;
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
SET v_error = TRUE;
-- Intentar insertar un duplicado (si hay UNIQUE en email)
INSERT INTO clientes (nombre, apellidos, email, ciudad, telefono)
VALUES ('Test', 'Handler', 'maria.garcia@email.com', 'Madrid', '600000000');
IF v_error THEN
SELECT 'Error: no se pudo insertar el registro' AS mensaje;
ELSE
SELECT 'Registro insertado correctamente' AS mensaje;
END IF;
END //
DELIMITER ;Al ejecutar el procedimiento (suponiendo que el email ya existe y hay un índice único sobre esa columna), el handler captura el error, marca la bandera y deja que la ejecución continúe hasta el IF final:
CALL insertar_con_handler();El resultado muestra el mensaje de la rama de error, lo que confirma que la ejecución no se detuvo en el INSERT:
| mensaje |
|---|
| Error: no se pudo insertar el registro |
La ejecución continúa después del error precisamente porque usamos CONTINUE HANDLER. Si hubiéramos usado EXIT, el procedimiento habría abandonado el bloque sin llegar al IF.
EXIT HANDLER
El comportamiento opuesto lo da EXIT HANDLER. Para apreciarlo bien, en el siguiente ejemplo encerramos la lógica en un bloque BEGIN ... END interno: cuando el INSERT falla, el handler asigna un mensaje y abandona ese bloque, de modo que la línea posterior (la que marcaría el insert como exitoso) nunca se ejecuta.
DELIMITER //
CREATE PROCEDURE exit_handler_ejemplo()
BEGIN
DECLARE v_resultado VARCHAR(100) DEFAULT 'No ejecutado';
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
SET v_resultado = 'Error capturado, saliendo del bloque';
END;
-- Esto genera un error (tabla inexistente)
INSERT INTO tabla_inexistente VALUES (1);
-- Esta línea NO se ejecuta con EXIT HANDLER
SET v_resultado = 'Insert exitoso';
END;
SELECT v_resultado AS resultado;
END //
DELIMITER ;Al llamarlo, el resultado demuestra que el flujo saltó directamente fuera del bloque interno tras el error, dejando la variable con el valor que asignó el handler:
CALL exit_handler_ejemplo();| resultado |
|---|
| Error capturado, saliendo del bloque |
Conviene recordar que EXIT sale solo del bloque BEGIN ... END donde está declarado el handler, no necesariamente de todo el procedimiento. Por eso anidar bloques permite controlar con precisión hasta dónde llega la salida.
Handler para errores específicos
A veces no quieres capturar cualquier error, sino uno muy concreto. MySQL identifica cada error con un número, y puedes declarar un handler para ese código exacto. El error 1062, por ejemplo, es el de entrada duplicada en un índice único. En el siguiente procedimiento el handler solo reacciona ante ese error en particular.
DELIMITER //
CREATE PROCEDURE manejar_duplicado(IN p_email VARCHAR(100))
BEGIN
DECLARE v_mensaje VARCHAR(200);
-- Error 1062: Duplicate entry
DECLARE CONTINUE HANDLER FOR 1062
SET v_mensaje = CONCAT('El email ', p_email, ' ya existe');
INSERT INTO clientes (nombre, apellidos, email, ciudad, telefono)
VALUES ('Nuevo', 'Cliente', p_email, 'Madrid', '600000000');
IF v_mensaje IS NULL THEN
SET v_mensaje = 'Cliente insertado correctamente';
END IF;
SELECT v_mensaje AS resultado;
END //
DELIMITER ;Si llamamos al procedimiento con un email que ya está registrado, salta el handler del error 1062 y obtenemos un mensaje específico para ese caso, en lugar de un error genérico:
CALL manejar_duplicado('maria.garcia@email.com');| resultado |
|---|
| El email maria.garcia@email.com ya existe |
Handler con SQLSTATE
En lugar del código numérico de MySQL, también puedes vigilar un código SQLSTATE, que es más portable porque forma parte del estándar SQL. El SQLSTATE '23000' agrupa todas las violaciones de integridad, así que un único handler para ese valor cubre tanto las claves foráneas como los índices únicos y las columnas obligatorias.
DELIMITER //
CREATE PROCEDURE handler_sqlstate()
BEGIN
DECLARE v_msg VARCHAR(200);
-- SQLSTATE 23000: Integrity constraint violation
DECLARE CONTINUE HANDLER FOR SQLSTATE '23000'
SET v_msg = 'Violación de restricción de integridad';
-- Intentar insertar con FK inválida
INSERT INTO pedidos (cliente_id, fecha, total, estado)
VALUES (9999, CURDATE(), 100.00, 'pendiente');
IF v_msg IS NOT NULL THEN
SELECT v_msg AS error;
END IF;
END //
DELIMITER ;Aquí, al insertar un pedido con un cliente_id que no existe, se viola la clave foránea, MySQL genera un error con SQLSTATE '23000' y el handler lo captura, devolviendo un mensaje genérico de integridad.
Handler con bloque BEGIN...END
Cuando la reacción al error necesita más de una instrucción, la sentencia del handler puede ser un bloque BEGIN ... END con varias líneas. Esto es lo habitual cuando, además de marcar una bandera, quieres recoger detalles del error con GET DIAGNOSTICS para mostrarlos o registrarlos.
DELIMITER //
CREATE PROCEDURE handler_complejo()
BEGIN
DECLARE v_error_code INT;
DECLARE v_error_msg VARCHAR(200);
DECLARE v_tiene_error BOOLEAN DEFAULT FALSE;
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1
v_error_code = MYSQL_ERRNO,
v_error_msg = MESSAGE_TEXT;
SET v_tiene_error = TRUE;
END;
-- Operación que podría fallar
INSERT INTO tabla_inexistente VALUES (1);
IF v_tiene_error THEN
SELECT v_error_code AS codigo, v_error_msg AS mensaje;
END IF;
END //
DELIMITER ;Dentro del handler, GET DIAGNOSTICS CONDITION 1 extrae el número de error y su mensaje y los guarda en variables. Así, al ejecutar el procedimiento, podemos devolver con exactitud qué falló:
CALL handler_complejo();| codigo | mensaje |
|---|---|
| 1146 | Table 'tienda_mysql.tabla_inexistente' doesn't exist |
Múltiples handlers
Un mismo bloque puede declarar varios handlers, cada uno para una condición distinta. MySQL elige cuál activar según cuál sea el error que se produzca, lo que te permite dar respuestas diferenciadas a cada tipo de problema dentro del mismo procedimiento.
DELIMITER //
CREATE PROCEDURE multiples_handlers()
BEGIN
DECLARE v_msg VARCHAR(200) DEFAULT 'OK';
DECLARE CONTINUE HANDLER FOR 1062
SET v_msg = 'Registro duplicado';
DECLARE CONTINUE HANDLER FOR 1452
SET v_msg = 'Clave foránea no válida';
DECLARE CONTINUE HANDLER FOR SQLWARNING
SET v_msg = 'Advertencia detectada';
-- Lógica del procedimiento...
SELECT v_msg AS estado;
END //
DELIMITER ;En este caso hay tres handlers: dos para errores numéricos concretos (1062 y 1452) y uno para la categoría general de advertencias. Cada uno asigna un mensaje distinto, de modo que la respuesta se adapta a la naturaleza exacta del problema.
Prioridad de handlers
Cuando una condición podría encajar en más de un handler, MySQL no elige al azar: aplica siempre el más específico. Saber este orden evita sorpresas cuando combinas handlers concretos con categorías generales. También puedes usar DECLARE CONDITION para dar nombres legibles a estas condiciones y que el código resulte más claro.
- Código de error MySQL específico
- SQLSTATE específico
- SQLEXCEPTION, SQLWARNING o NOT FOUND
Dicho de otro modo, un handler para el error 1062 tiene prioridad sobre uno para el SQLSTATE '23000', y este a su vez sobre uno genérico para SQLEXCEPTION. Esto te permite tratar ciertos errores de forma especial y dejar el resto a un handler general.
Errores comunes
El error más frecuente es declarar los handlers en el lugar equivocado. En un bloque BEGIN ... END, primero van las variables (DECLARE ... ), después las condiciones, luego los handlers y, por último, el resto de sentencias. Colocar un DECLARE HANDLER después de una instrucción ejecutable provoca un error de sintaxis.
Otro malentendido habitual tiene que ver con NOT FOUND y los cursores. Cuando recorres un cursor, MySQL lanza la condición NOT FOUND al llegar al final de los datos. Si declaras un CONTINUE HANDLER FOR NOT FOUND demasiado amplio, puede capturar también el final de otros cursores o de sentencias SELECT ... INTO que no devuelven filas, provocando comportamientos inesperados. Conviene acotar bien cuándo se activa y, si es necesario, usar banderas para controlar el bucle.
Cuándo usar DECLARE HANDLER
Usa DECLARE HANDLER siempre que un procedimiento realice operaciones que puedan fallar y quieras controlar ese fallo en lugar de dejar que aborte sin más. Es imprescindible para recorrer cursores (necesitas un handler NOT FOUND), muy recomendable en procesos por lotes donde un registro problemático no debe tumbar todo el proceso, y la base sobre la que se apoyan SIGNAL y RESIGNAL para construir un manejo de errores completo.
En el siguiente artículo veremos DECLARE CONDITION, que permite dar nombres legibles a las condiciones de error y hace que las declaraciones de handler sean mucho más fáciles de leer.
Limpieza
Para terminar, eliminamos todos los procedimientos creados en los ejemplos.
DROP PROCEDURE IF EXISTS insertar_con_handler;
DROP PROCEDURE IF EXISTS exit_handler_ejemplo;
DROP PROCEDURE IF EXISTS manejar_duplicado;
DROP PROCEDURE IF EXISTS handler_sqlstate;
DROP PROCEDURE IF EXISTS handler_complejo;
DROP PROCEDURE IF EXISTS multiples_handlers;Escrito por Eduardo Lázaro
