Menangani kesalahan dengan TRY...CATCH

Selesai

Tip

Lihat tab Teks dan gambar untuk detail selengkapnya!

Aplikasi database produksi harus menangani situasi yang tidak terduga dengan anggun. Pembagian dengan nol, pelanggaran batasan, batas waktu koneksi, dan data yang tidak valid semuanya dapat menyebabkan kesalahan. Kesalahan yang tidak tertangani mengakibatkan pesan kesalahan yang tidak jelas, transaksi yang tidak lengkap, atau crash aplikasi. Penanganan kesalahan yang tepat memastikan kode T-SQL Anda gagal secara terprediksi dan memberikan umpan balik yang bermakna.

Operasi database berinteraksi dengan beberapa pengguna, sistem eksternal, dan input data yang tidak dapat diprediksi secara bersamaan. Tidak seperti kode aplikasi yang mungkin pulih dari operasi yang gagal dengan mencoba kembali, kesalahan database dapat meninggalkan data dalam keadaan tidak konsisten, dengan beberapa baris dimasukkan dan yang lain tidak, atau dengan kunci yang disimpan tanpa batas waktu. Penanganan kesalahan mengubah mode kegagalan kacau ini menjadi respons yang terkontrol dan dapat diprediksi.

Penanganan kesalahan yang dirancang dengan baik meningkatkan kode Anda dengan beberapa cara:

  • Perlindungan integritas data: Ketika operasi gagal sebagian, penanganan kesalahan yang tepat memastikan bahwa semua perubahan diterapkan secara bersamaan atau tidak ada perubahan yang tersimpan sama sekali. Tanpa ini, proses bertahap mungkin meninggalkan database Anda dengan rekaman terlantar, total yang tidak cocok, atau hubungan yang rusak.

  • Efisiensi penelusuran kesalahan: Menangkap detail kesalahan seperti nomor baris, nama prosedur, dan pesan kesalahan tertentu membuat pemecahan masalah lebih cepat. Alih-alih mencari log untuk kegagalan yang samar, Anda dapat menentukan dengan tepat di mana dan mengapa kesalahan terjadi.

  • Pengalaman pengguna: Aplikasi dapat menampilkan pesan yang bermakna seperti "ID produk tidak ada" alih-alih kesalahan database kriptik. Ini membantu pengguna memahami apa yang salah dan cara memperbaikinya.

  • Visibilitas operasional: Pencatatan kesalahan ke tabel khusus untuk audit membantu mengidentifikasi pola, seperti pelanggaran batasan berulang yang menunjukkan bug atau kesalahan batas waktu yang menunjukkan masalah kinerja.

  • Degradasi yang terkontrol: Ketika satu operasi gagal, penanganan kesalahan memungkinkan sisa kode Anda untuk melanjutkan atau mengambil tindakan alternatif, daripada menghentikan seluruh batch atau prosedur tersimpan.

Menerapkan penanganan kesalahan T-SQL

T-SQL menyediakan penanganan kesalahan terstruktur melalui TRY...CATCH blok, mirip dengan penanganan pengecualian dalam bahasa pemrograman lainnya. Ketika kesalahan terjadi di TRY blok, eksekusi akan ditransfer ke CATCH blok tempat Anda dapat menangani kesalahan dengan tepat:

BEGIN TRY
    -- TRY block contains code that might cause an error
    -- If an error occurs here, execution jumps to the CATCH block
    SELECT 1/0;  -- This causes a division by zero error
END TRY
BEGIN CATCH
    -- CATCH block handles the error
    -- This code runs only if an error occurred in the TRY block
    PRINT 'An error occurred';
END CATCH;

Tanpa penanganan kesalahan, kode yang sama akan berakhir dengan pesan kesalahan:

SELECT 1/0;  -- Msg 8134: Divide by zero error encountered

Nota

TRY...CATCH tidak dapat menangkap semua kesalahan. Kesalahan kompilasi (kesalahan sintaks, objek yang hilang) dan kesalahan dengan tingkat keparahan 20 atau lebih tinggi yang menutup koneksi tidak dapat ditangkap dalam sesi yang sama.

Mendapatkan informasi kesalahan

CATCH Dalam blok, SQL Server menyediakan fungsi berikut untuk mengambil detail tentang kesalahan yang terjadi:

Fungsi Deskripsi
ERROR_NUMBER() Mengembalikan nomor kesalahan
ERROR_MESSAGE() Mengembalikan teks pesan kesalahan lengkap
ERROR_SEVERITY() Mengembalikan tingkat keparahan kesalahan (0-25)
ERROR_STATE() Mengembalikan nomor status kesalahan
ERROR_LINE() Mengembalikan nomor baris tempat kesalahan terjadi
ERROR_PROCEDURE() Mengembalikan nama prosedur atau pemicu tersimpan

Contoh berikut menunjukkan cara mengambil detail kesalahan dan mencatatnya ke tabel untuk analisis nanti:

BEGIN TRY
    -- Attempt an operation that might fail
    INSERT INTO SalesLT.Customer (CustomerID, FirstName, LastName)
    VALUES (1, 'Test', 'Customer');  -- Duplicate key causes error
END TRY
BEGIN CATCH
    -- Log error details to a table using the ERROR_* functions
    INSERT INTO ErrorLog (
        ErrorTime,
        ErrorNumber,
        ErrorSeverity,
        ErrorState,
        ErrorProcedure,
        ErrorLine,
        ErrorMessage
    )
    VALUES (
        GETDATE(),
        ERROR_NUMBER(),       -- The error number (e.g., 2627 for duplicate key)
        ERROR_SEVERITY(),     -- Severity level (0-25)
        ERROR_STATE(),        -- Error state for debugging
        ISNULL(ERROR_PROCEDURE(), 'Ad hoc query'),  -- NULL if not in a procedure
        ERROR_LINE(),         -- Line number where error occurred
        ERROR_MESSAGE()       -- Full error message text
    );

    -- Re-raise the error to the calling application
    THROW;
END CATCH;

Tip

Selalu catat kesalahan sebelum mengurainya kembali. Setelah Anda menggunakan THROW atau RAISERROR, fungsi kesalahan akan kembali NULL jika dipanggil lagi.

Menangani transaksi dengan TRY... CATCH

Ketika kesalahan terjadi dalam transaksi, Anda harus secara eksplisit mengembalikan pekerjaan yang tidak dilakukan. Fungsi ini @@TRANCOUNT memberi tahu Anda apakah transaksi aktif:

BEGIN TRY
    -- Start a transaction to group multiple operations
    BEGIN TRANSACTION;

    -- First operation: update product prices
    UPDATE SalesLT.Product
    SET ListPrice = ListPrice * 1.05
    WHERE ProductCategoryID = 5;

    -- Second operation: update order totals
    -- If this fails, we want to undo the first update too
    UPDATE SalesLT.SalesOrderHeader
    SET TotalDue = TotalDue * 1.05
    WHERE CustomerID = 12345;

    -- Both operations succeeded, make changes permanent
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    -- Check if a transaction is still active before rolling back
    -- Some errors auto-rollback, so @@TRANCOUNT might be 0
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;  -- Undo all changes from this transaction

    -- Re-raise the error so the caller knows something failed
    THROW;
END CATCH;

Pemeriksaan @@TRANCOUNT ini penting karena:

  • Kesalahan mungkin terjadi sebelumnya BEGIN TRANSACTION
  • Beberapa kesalahan secara otomatis mengembalikan transaksi sebelum mencapai CATCH
  • Mencoba untuk memutar kembali tanpa transaksi aktif menyebabkan kesalahan lain

Penting

Selalu periksa @@TRANCOUNT sebelum memanggil ROLLBACK TRANSACTION di blok CATCH. Ini mencegah kesalahan "permintaan ROLLBACK TRANSACTION tidak memiliki BEGIN TRANSACTION yang sesuai."

Meningkatkan kesalahan kustom dengan THROW

Pernyataan THROW ini menghasilkan pengecualian dengan nomor kesalahan dan pesan khusus. Gunakan untuk memberi sinyal kondisi kesalahan khusus aplikasi:

CREATE PROCEDURE ProcessOrder
    @OrderID INT,
    @Quantity INT
AS
BEGIN
    BEGIN TRY
        -- Validate input and raise custom errors for invalid data
        IF @Quantity <= 0
            THROW 50001, 'Quantity must be greater than zero.', 1;

        IF NOT EXISTS (SELECT 1 FROM Orders WHERE OrderID = @OrderID)
            THROW 50002, 'Order not found.', 1;

        -- Process the order
        UPDATE Orders
        SET Quantity = @Quantity
        WHERE OrderID = @OrderID;

    END TRY
    BEGIN CATCH
        -- Log the error before reraising
        EXEC LogError;

        -- THROW without parameters reraises the current error
        THROW;
    END CATCH;
END;

Nomor kesalahan kustom untuk kesalahan yang ditentukan pengguna harus 50000 atau lebih tinggi. Parameter status (1 dalam contoh) adalah nilai yang ditentukan pengguna antara 1 dan 255 yang dapat membantu mengidentifikasi tempat kesalahan dimunculkan.

Gunakan RAISERROR untuk pesan yang diformat

RAISERROR menyediakan lebih banyak opsi pemformatan daripada THROW, termasuk penggantian parameter gaya cetak. Menyertakan nilai runtime dalam pesan kesalahan membuat penelusuran kesalahan lebih mudah karena Anda dapat melihat dengan tepat data mana yang menyebabkan kegagalan tanpa menggali log atau mereproduksi masalah:

DECLARE @ProductName NVARCHAR(100) = 'Widget Pro';
DECLARE @CurrentStock INT = 5;
DECLARE @RequestedQty INT = 10;

IF @CurrentStock < @RequestedQty
BEGIN
    RAISERROR(
        'Insufficient stock for product "%s". Available: %d, Requested: %d',
        16,  -- Severity
        1,   -- State
        @ProductName,
        @CurrentStock,
        @RequestedQty
    );
END;

Nota

THROW adalah pendekatan yang direkomendasikan untuk kode baru karena lebih sederhana dan selalu menyertakan jejak tumpukan. Gunakan RAISERROR saat Anda memerlukan pesan atau kompatibilitas yang diformat dengan pola penanganan kesalahan yang ada.

Menerapkan penanganan kesalahan berlapis

Prosedur tersimpan yang memanggil prosedur lain memerlukan penanganan kesalahan terkoordinasi. Setiap tingkat harus menangani kesalahan pembersihan dan penyebarannya sendiri dengan tepat:

CREATE PROCEDURE OuterProcedure
AS
BEGIN
    BEGIN TRY
        -- Outer procedure owns the transaction
        BEGIN TRANSACTION;

        -- First operation in the outer procedure
        UPDATE SomeTable SET Column1 = 'Value';

        -- Call nested procedure - if it fails, error propagates here
        EXEC InnerProcedure;

        -- All operations succeeded, commit the transaction
        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        -- Outer procedure handles rollback for all nested calls
        IF @@TRANCOUNT > 0
            ROLLBACK TRANSACTION;

        -- Propagate error to the application
        THROW;
    END CATCH;
END;
GO

CREATE PROCEDURE InnerProcedure
AS
BEGIN
    BEGIN TRY
        -- Inner procedure does its work within the outer's transaction
        UPDATE AnotherTable SET Column2 = 'Value';
    END TRY
    BEGIN CATCH
        -- Don't rollback here - let the outer procedure handle it
        -- This keeps transaction management in one place
        THROW;  -- Re-raise error to outer procedure
    END CATCH;
END;

Gunakan XACT_ABORT untuk putar kembali otomatis

Anda dapat mengatur XACT_ABORT ON untuk menyebabkan SQL Server secara otomatis mengembalikan transaksi ketika kesalahan terjadi, bahkan tanpa TRY...CATCH seperti ini:

SET XACT_ABORT ON;

BEGIN TRANSACTION;
    UPDATE Table1 SET Col1 = 'A';
    UPDATE Table2 SET Col2 = 'B';  -- If this fails, entire transaction rolls back
    UPDATE Table3 SET Col3 = 'C';
COMMIT TRANSACTION;

XACT_ABORT Menggabungkan dengan TRY...CATCH memberi Anda manfaat dari kedua pendekatan: XACT_ABORT menjamin pembatalan segera untuk kesalahan apa pun, sambil TRY...CATCH memungkinkan Anda mencatat detail kesalahan dan melakukan pembersihan kustom sebelum kesalahan menyebar:

-- XACT_ABORT ON ensures automatic rollback on any error
SET XACT_ABORT ON;

BEGIN TRY
    BEGIN TRANSACTION;

    -- Execute multiple procedures as a single unit of work
    EXEC Procedure1;  -- If any of these fail...
    EXEC Procedure2;  -- ...XACT_ABORT automatically rolls back...
    EXEC Procedure3;  -- ...and jumps to the CATCH block

    -- All succeeded, commit the changes
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    -- With XACT_ABORT ON, the transaction is usually already rolled back
    -- This check handles edge cases where it might still be active
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;

    -- Log the error details before re-raising
    EXEC LogError;

    -- Let the caller know an error occurred
    THROW;
END CATCH;

Tip

Menggunakan SET XACT_ABORT ON adalah praktik terbaik untuk prosedur tersimpan, terutama yang mencakup beberapa operasi. Ini memastikan perilaku yang konsisten terlepas dari kesalahan spesifik yang terjadi.

Untuk informasi selengkapnya tentang penanganan kesalahan, lihat TRY... CATCH (Transact-SQL) dan THROW (Transact-SQL).