CodeGym /Cursos /SQL SELF /Manejo de errores y volver al estado original: EXCEPTION,...

Manejo de errores y volver al estado original: EXCEPTION, RAISE

SQL SELF
Nivel 53 , Lección 3
Disponible

Los errores en PostgreSQL pueden aparecer por muchas razones: violación de restricciones (NOT NULL, UNIQUE, CHECK), errores de sintaxis, valores duplicados, etc. Si no capturas y manejas estos errores, toda la transacción externa puede ser revertida completamente. Para operaciones de negocio estables, es importante manejarlos correctamente.

PL/pgSQL te da un mecanismo potente de bloques BEGIN ... EXCEPTION ... END para capturar y manejar errores en funciones y procedimientos. Son parecidos a los try-catch de Python o Java, pero con sus particularidades en el contexto de transacciones de PostgreSQL.

Punto importante:

Cada bloque BEGIN ... EXCEPTION ... END funciona como un “savepoint virtual”. Si ocurre una excepción, todos los cambios de ese bloque se revierten automáticamente. Es la única forma correcta de hacer rollback parcial en funciones y procedimientos en PL/pgSQL.

Sintaxis de manejo de errores con EXCEPTION

BEGIN
    -- código principal
EXCEPTION
    WHEN TIPO_DE_ERROR THEN
        -- manejo de error específico
    WHEN OTRO_TIPO_DE_ERROR THEN
        -- otro manejador
    WHEN OTHERS THEN
        -- manejo de todos los demás errores
END;

Ilustración en funciones/procedimientos

DO $$
BEGIN
    RAISE NOTICE 'Ahora va a ocurrir un error...';
    PERFORM 1 / 0;
EXCEPTION
    WHEN division_by_zero THEN
        RAISE NOTICE '¡División por cero capturada!';
END;
$$;

Ejemplo: actualización con manejo y rollback de errores

Supón que tienes una tabla de pedidos:

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    amount NUMERIC NOT NULL,
    status TEXT NOT NULL
);

Vamos a crear una función que actualiza el estado del pedido y si hay error no cambia nada:

CREATE OR REPLACE FUNCTION update_order_status(order_id INT, new_status TEXT)
RETURNS VOID AS $$
BEGIN
    BEGIN
        UPDATE orders
        SET status = new_status
        WHERE id = order_id;

        -- Simulación de error
        IF new_status = 'FAIL' THEN
            RAISE EXCEPTION '¡Simulando error!';
        END IF;

        RAISE NOTICE 'Estado del pedido % actualizado', order_id;

    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE 'Error al actualizar el pedido %: %', order_id, SQLERRM;
            -- ¡Todos los cambios dentro del bloque se revierten automáticamente!
            -- Relanzamos el error para manejo externo si hace falta
            RAISE;
    END;
END;
$$ LANGUAGE plpgsql;

Cómo funciona esto en procedimientos con manejo explícito de transacciones

En procedimientos (CREATE PROCEDURE) puedes usar COMMIT, ROLLBACK, SAVEPOINT, pero no puedes hacer ROLLBACK TO SAVEPOINT. Si necesitas revertir solo una parte de las operaciones dentro del procedimiento, usa el mismo BEGIN ... EXCEPTION ... END:

CREATE OR REPLACE PROCEDURE pay_order(order_id INT, amount NUMERIC)
LANGUAGE plpgsql
AS $$
BEGIN
    -- Todo el procedimiento puede usar COMMIT/ROLLBACK, pero para revertir solo una etapa:
    BEGIN
        UPDATE accounts
        SET balance = balance - amount
        WHERE id = (SELECT account_id FROM orders WHERE id = order_id);

        -- error
        IF amount < 0 THEN
            RAISE EXCEPTION '¡La cantidad no puede ser negativa!';
        END IF;

        UPDATE orders SET status = 'PAID' WHERE id = order_id;

    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE 'Error al procesar el pago del pedido %: %', order_id, SQLERRM;
            -- los cambios en este bloque se revierten automáticamente
    END;

    COMMIT; -- ¡Solo puedes finalizar la transacción explícitamente en procedimientos!
END;
$$;

Registro de errores (logging)

No solo es importante capturar errores, sino también guardarlos para analizarlos después.

CREATE TABLE error_log (
    id SERIAL PRIMARY KEY,
    order_id INT,
    error_message TEXT,
    error_time TIMESTAMP DEFAULT now()
);

En una función o procedimiento:

EXCEPTION
    WHEN OTHERS THEN
        INSERT INTO error_log (order_id, error_message)
        VALUES (order_id, SQLERRM);
        RAISE NOTICE 'Error registrado en el log: %', SQLERRM;
        RAISE;

Limitaciones y matices importantes en PostgreSQL 17

¡Dentro de funciones (CREATE FUNCTION ... ) no puedes usar comandos de manejo de transacciones (BEGIN, COMMIT, ROLLBACK, SAVEPOINT, ROLLBACK TO SAVEPOINT, RELEASE SAVEPOINT)! Todas las funciones se ejecutan completamente dentro de la transacción externa.

Dentro de procedimientos (CREATE PROCEDURE ... ) puedes escribir explícitamente SAVEPOINT, RELEASE SAVEPOINT, COMMIT, ROLLBACK. PERO: ROLLBACK TO SAVEPOINT — ¡PROHIBIDO! (Vas a recibir un error de sintaxis si intentas usar ROLLBACK TO SAVEPOINT en un procedimiento PL/pgSQL).

El rollback de "parte del código" dentro de funciones y procedimientos se hace con bloques BEGIN ... EXCEPTION ... END. Si ocurre un error, todo dentro del bloque se revierte automáticamente y la ejecución puede continuar.

Los procedimientos (CREATE PROCEDURE) no se pueden ejecutar dentro de una función ni mediante SELECT — solo con el comando CALL ... por separado.

¿Cómo hacer realmente un "rollback parcial" en PL/pgSQL?

La única forma que funciona — usar manejo de errores con bloques BEGIN ... EXCEPTION ... END. Este bloque crea automáticamente un savepoint, y si hay error revierte los cambios dentro del bloque, sin afectar el resto del procedimiento/función.

Ejemplo con EXCEPTION (forma recomendada):

CREATE OR REPLACE PROCEDURE demo_savepoint()
LANGUAGE plpgsql
AS $$
BEGIN
    -- Algún código
    BEGIN
        -- Aquí un error no revierte todo el procedimiento,
        -- ¡solo este bloque!
        INSERT INTO demo VALUES ('datos malos'); -- puede causar error
    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE 'Error manejado, los cambios dentro del bloque han sido cancelados';
    END;
    -- ¡Aquí la ejecución sigue!
END;
$$;

Ejemplo: carga de lote de datos con protección contra rollback total

CREATE OR REPLACE PROCEDURE load_big_batch()
LANGUAGE plpgsql
AS $$
DECLARE
    rec RECORD;
BEGIN
    FOR rec IN SELECT * FROM import_table LOOP
        BEGIN
            INSERT INTO target_table (col1, col2)
            VALUES (rec.col1, rec.col2);
        EXCEPTION WHEN OTHERS THEN
            INSERT INTO import_errors (err_msg)
            VALUES ('Error en el registro: ' || rec.col1 || ': ' || SQLERRM);
            -- ¡los cambios dentro de este bloque han sido cancelados!
        END;
    END LOOP;
    COMMIT;  -- solo permitido si el procedimiento se ejecuta fuera de una transacción externa explícita
END;
$$;

-- Llamada al procedimiento
CALL load_big_batch();

Ojo: si llamas a este procedimiento desde un cliente que ya tiene una transacción abierta (por ejemplo, Python con autocommit=False), ejecutar COMMIT o SAVEPOINT dentro del procedimiento dará error.

Consejos para trabajar con savepoints anidados y EXCEPTION

  1. ¡No uses ROLLBACK TO SAVEPOINT en PL/pgSQL! Eso da error de sintaxis.
  2. Para rollback parcial, usa siempre bloques anidados BEGIN ... EXCEPTION ... END.
  3. No olvides que COMMIT y ROLLBACK dentro de procedimientos reinician la transacción — ¡solo los puedes usar si el procedimiento se ejecuta en modo autocommit!
  4. Registra los errores en una tabla aparte para no perder info sobre filas incorrectas.
  5. Si la operación de negocio debe ser estrictamente atómica (todo-o-nada) — hazla como función, sin COMMIT/ROLLBACK dentro; si necesitas procesamiento por etapas — hazlo como procedimiento.

Ejemplo: importación por lotes con manejo parcial

CREATE OR REPLACE PROCEDURE import_batch()
LANGUAGE plpgsql
AS $$
DECLARE
    rec RECORD;
BEGIN
    FOR rec IN SELECT * FROM staging_table LOOP
        BEGIN
            INSERT INTO data_table (data)
            VALUES (rec.data);
        EXCEPTION
            WHEN unique_violation THEN
                INSERT INTO import_log (msg)
                VALUES ('Duplicado: ' || rec.data);
            WHEN OTHERS THEN
                INSERT INTO import_log (msg)
                VALUES ('Error: ' || rec.data || ' — ' || SQLERRM);
        END;
    END LOOP;
END;
$$;

Lo principal que tienes que recordar para PostgreSQL 17:

En procedimientos PL/pgSQL están permitidos SAVEPOINT, RELEASE SAVEPOINT, COMMIT, ROLLBACK, pero ROLLBACK TO SAVEPOINT — no.

El "rollback parcial" dentro de funciones y procedimientos solo se hace con bloques anidados BEGIN ... EXCEPTION ... END.

Es mejor manejar las transacciones desde fuera (con parámetros de conexión y autocommit), y dentro de los procedimientos usar los mecanismos de manejo de errores que hemos visto.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION