RESIGNAL
RESIGNAL es la sentencia que permite, desde dentro de un handler de error, volver a lanzar la condición que se acaba de capturar, opcionalmente cambiando alguna de sus propiedades antes de propagarla. Mientras que SIGNAL crea un error nuevo desde cero, RESIGNAL trabaja siempre sobre un error que ya existe: el que activó el handler en el que te encuentras.
Esta diferencia es lo que hace a RESIGNAL tan valioso para construir capas de manejo de errores. El patrón habitual es interceptar un error técnico de bajo nivel (una violación de clave foránea, una tabla inexistente, un valor fuera de rango), registrarlo en un log o añadirle contexto comprensible, y después relanzarlo para que la aplicación que llamó al procedimiento se entere igualmente de que algo falló. Sin RESIGNAL, un handler que captura el error lo "consume" y la operación parece haber tenido éxito cuando no fue así. En este artículo veremos cómo relanzar un error tal cual, cómo enriquecer su mensaje y cómo transformarlo en algo más adecuado para quien lo recibe.
Sintaxis
RESIGNAL admite tres formas, de la más simple a la más completa. La primera relanza el error sin tocar nada; la segunda mantiene el SQLSTATE original pero cambia propiedades como el mensaje; la tercera sustituye también el SQLSTATE por uno nuevo.
-- Relanzar el mismo error
RESIGNAL;
-- Relanzar modificando propiedades
RESIGNAL
SET MESSAGE_TEXT = 'Nuevo mensaje';
-- Relanzar con nuevo SQLSTATE
RESIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Error transformado';La clave que comparten las tres es que solo pueden aparecer dentro de un handler. Usar RESIGNAL fuera de ese contexto es un error de sintaxis, porque no hay ninguna condición previa que relanzar.
Relanzar el mismo error
El uso más directo es registrar el error y propagarlo sin alterarlo. En el siguiente procedimiento, un handler de tipo EXIT captura cualquier excepción, simula el registro en un log y luego llama a RESIGNAL sin argumentos para que el error original siga su curso hacia el llamador.
DELIMITER //
CREATE PROCEDURE ejemplo_resignal_basico()
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
-- Registrar el error (en producción: INSERT en tabla de logs)
SELECT 'Error registrado en log' AS log;
-- Relanzar el error original
RESIGNAL;
END;
-- Esto genera un error
INSERT INTO tabla_inexistente VALUES (1);
END //
DELIMITER ;Al llamar a este procedimiento, primero se ejecuta la lógica del handler (que muestra el log) y, acto seguido, RESIGNAL propaga el error original al llamador con su código y mensaje intactos. Es decir, el error queda registrado pero la aplicación sigue recibiendo la notificación de fallo, que es exactamente lo que queremos en una capa de auditoría.
Modificar el mensaje
A menudo el mensaje técnico que genera MySQL no dice nada útil a quien lee el error desde fuera. Con RESIGNAL ... SET MESSAGE_TEXT puedes envolver ese mensaje en uno más descriptivo que incluya el contexto del negocio, como el cliente o la operación implicados. El siguiente procedimiento de creación de pedidos captura el error, recupera su texto con GET DIAGNOSTICS y lo relanza añadiendo información sobre qué cliente provocó el fallo.
DELIMITER //
CREATE PROCEDURE crear_pedido_seguro(
IN p_cliente_id INT,
IN p_producto_id INT,
IN p_cantidad INT
)
BEGIN
DECLARE v_error_original VARCHAR(200);
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1
v_error_original = MESSAGE_TEXT;
RESIGNAL SET MESSAGE_TEXT =
CONCAT('Error al crear pedido para cliente ', p_cliente_id, ': ', v_error_original);
END;
-- Insertar pedido (puede fallar por FK, etc.)
INSERT INTO pedidos (cliente_id, fecha, total, estado)
VALUES (p_cliente_id, CURDATE(), 0, 'pendiente');
INSERT INTO detalle_pedidos (pedido_id, producto_id, cantidad, precio_unitario)
VALUES (LAST_INSERT_ID(), p_producto_id, p_cantidad,
(SELECT precio FROM productos WHERE id = p_producto_id));
END //
DELIMITER ;Si cualquiera de las dos inserciones falla, por ejemplo porque el cliente no existe y se viola una clave foránea, el handler intercepta el error, conserva su mensaje original y lo relanza precedido de un texto que identifica al cliente y la operación. Quien recibe el error ya no ve solo "Cannot add or update a child row", sino un mensaje que le dice dónde mirar.
Transformar errores
En sistemas que exponen una API conviene no filtrar detalles internos de la base de datos hacia el exterior. La tercera forma de RESIGNAL, con un SQLSTATE nuevo, te permite atrapar cualquier error de bajo nivel y sustituirlo por uno genérico y controlado de cara al cliente, ocultando la causa técnica concreta.
DELIMITER //
CREATE PROCEDURE api_actualizar_precio(
IN p_producto_id INT,
IN p_nuevo_precio DECIMAL(10,2)
)
BEGIN
-- Capturar cualquier error de bajo nivel
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
-- Transformar en un error amigable para la API
RESIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Error interno al actualizar el precio. Contacte al administrador.';
END;
IF p_nuevo_precio <= 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'El precio debe ser positivo';
END IF;
UPDATE productos SET precio = p_nuevo_precio WHERE id = p_producto_id;
IF ROW_COUNT() = 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Producto no encontrado';
END IF;
SELECT 'Precio actualizado' AS resultado;
END //
DELIMITER ;Observa que aquí conviven SIGNAL y RESIGNAL: los SIGNAL lanzan errores de validación con mensajes claros pensados para el usuario, mientras que el handler con RESIGNAL actúa como red de seguridad para cualquier error inesperado de más bajo nivel, convirtiéndolo en un mensaje genérico. Es un buen ejemplo de cómo combinar ambos según el origen del error.
RESIGNAL vs SIGNAL
Aunque se escriben de forma parecida, SIGNAL y RESIGNAL responden a necesidades distintas. La siguiente tabla resume cuándo encaja cada uno.
| Característica | SIGNAL | RESIGNAL |
|---|---|---|
| Contexto | Cualquier lugar | Solo dentro de un handler |
| Crea nuevo error | Sí | No, propaga el existente |
| Puede modificar | Todas las propiedades | Solo las que especifiques |
| Sin argumentos | No válido | Relanza el error tal cual |
La regla práctica es sencilla: usa SIGNAL cuando tú decides que algo es un error según tus reglas de negocio, y usa RESIGNAL cuando ya hay un error en curso dentro de un handler y quieres registrarlo, enriquecerlo o transformarlo antes de dejarlo continuar.
RESIGNAL sin argumentos
La forma sin argumentos merece una mención aparte porque es la más fiel: relanza el error exactamente como fue capturado, sin cambiar ni el SQLSTATE ni el mensaje ni el código. Es ideal para envoltorios cuyo único cometido es registrar el error sin interferir en su contenido, de modo que las capas superiores reciban la información original.
DELIMITER //
CREATE PROCEDURE wrapper_con_log()
BEGIN
DECLARE v_error_code INT;
DECLARE v_error_msg VARCHAR(200);
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1
v_error_code = MYSQL_ERRNO,
v_error_msg = MESSAGE_TEXT;
-- Registrar error
-- INSERT INTO error_log (codigo, mensaje, fecha) VALUES (v_error_code, v_error_msg, NOW());
-- Propagar sin modificar
RESIGNAL;
END;
-- Lógica del procedimiento...
SELECT * FROM tabla_inexistente;
END //
DELIMITER ;Aquí el handler captura el código y el mensaje en variables (para poder registrarlos), pero al relanzar con RESIGNAL a secas no toca el error: la capa que llamó a wrapper_con_log recibirá el error 1146 original, no una versión alterada.
Ejemplo práctico: capas de error
Donde RESIGNAL despliega todo su potencial es en arquitecturas por capas, en las que un procedimiento llama a otro y cada nivel quiere aportar su parte de contexto al mensaje. En el siguiente ejemplo, una "capa de datos" prefija el error con su etiqueta y una "capa de negocio" añade la suya por encima, de manera que el mensaje final muestra el recorrido completo del fallo.
DELIMITER //
CREATE PROCEDURE capa_datos(IN prod_id INT)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
RESIGNAL SET MESSAGE_TEXT = CONCAT('[DATOS] Error al acceder producto ', prod_id);
END;
SELECT nombre, precio FROM productos WHERE id = prod_id;
END //
CREATE PROCEDURE capa_negocio(IN prod_id INT)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
RESIGNAL SET MESSAGE_TEXT = CONCAT('[NEGOCIO] ', MESSAGE_TEXT);
END;
IF prod_id <= 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'ID de producto inválido';
END IF;
CALL capa_datos(prod_id);
END //
DELIMITER ;Cada capa añade su prefijo al mensaje conforme el error asciende, así que al final obtienes una traza legible del tipo [NEGOCIO] [DATOS] ... que indica por qué niveles pasó el problema. Es una técnica muy útil para depurar lógica almacenada compleja sin tener que adivinar dónde se originó el fallo.
Errores comunes
El tropiezo más típico es intentar usar RESIGNAL fuera de un handler, lo que MySQL rechaza siempre. Solo tiene sentido cuando hay una condición activa que relanzar. Relacionado con esto, conviene recordar que un RESIGNAL colocado en un handler de tipo CONTINUE propaga el error igual que en uno EXIT: la diferencia entre CONTINUE y EXIT afecta a qué pasaría si el handler terminara sin relanzar, no al efecto del propio RESIGNAL.
Otro descuido frecuente es modificar el mensaje pero olvidar capturar antes el original con GET DIAGNOSTICS. Si concatenas MESSAGE_TEXT dentro del SET sin haberlo leído previamente en una variable, en algunas situaciones puedes acabar con un mensaje incompleto. Por seguridad, lee primero las propiedades que necesites y luego constrúyelo.
Cuándo usar RESIGNAL
RESIGNAL es la herramienta adecuada siempre que un handler no deba "tragarse" el error de forma silenciosa. Si tu intención es registrar el problema y seguir notificándolo, o traducir un error técnico a uno comprensible, o construir trazas por capas, RESIGNAL es la pieza que necesitas. En cambio, si de verdad quieres recuperarte del error y continuar como si nada (por ejemplo, asignar un valor por defecto), entonces no relances: deja que el handler maneje la situación y termine.
En el siguiente artículo veremos SHOW ERRORS, que permite inspeccionar de forma interactiva los errores generados por la última sentencia.
Limpieza
Para terminar, eliminamos todos los procedimientos de ejemplo. Usamos IF EXISTS para que el comando no falle aunque alguno no se haya llegado a crear.
DROP PROCEDURE IF EXISTS ejemplo_resignal_basico;
DROP PROCEDURE IF EXISTS crear_pedido_seguro;
DROP PROCEDURE IF EXISTS api_actualizar_precio;
DROP PROCEDURE IF EXISTS wrapper_con_log;
DROP PROCEDURE IF EXISTS capa_datos;
DROP PROCEDURE IF EXISTS capa_negocio;Escrito por Eduardo Lázaro
