Pendahuluan
Pemahaman try-catch di Laravel membantu developer pemula hingga menengah menulis kode yang lebih robust dan mudah dirawat. Laravel sudah menyediakan mekanisme penanganan error global melalui exception handler, tetapi dalam praktik kita tetap perlu menempatkan blok try-catch manual di titik-titik kritis seperti controller, service, job, dan transaksi database. Artikel ini membahas cara memanfaatkan try-catch secara efektif, perbedaan Exception vs Throwable, serta best practice saat membuat custom exception.
Dasar: Exception vs Throwable di PHP
Sejak PHP 7, hierarki error memiliki interface Throwable yang diimplementasikan oleh dua turunan utama: Exception dan Error. Semua exception userland—termasuk bawaan Laravel seperti ValidationException—adalah turunan Exception. Sementara Error mencakup error fatal (misal TypeError, ParseError) yang biasanya mengindikasikan bug tingkat bahasa.
- Menangkap Exception: digunakan ketika Anda ingin memproses error terduga yang masih bisa ditangani, misalnya kegagalan query, validasi, atau integrasi API.
- Menangkap Throwable: berguna pada boundary yang ingin “fail-safe” terhadap semua kondisi, seperti job queue critical atau scheduler, tetapi harus disertai fallback atau rethrow agar tidak menutupi bug fatal.
- Risiko menangkap terlalu luas: menangkap
Throwabletanpa rethrow dapat menyembunyikan bug serius. Gunakan logging dan, bila perlu, lempar ulang agar monitoring tetap memantau.
Arsitektur Penanganan Error di Laravel
Exception Handler Global
File app/Exceptions/Handler.php mengelola pelaporan (report()) dan rendering (render()). Semua exception yang tidak tertangkap secara manual akan mengalir ke handler ini. Handler cocok untuk aturan global seperti standarisasi response JSON atau integrasi Sentry. Kelemahannya, Anda tidak punya konteks spesifik untuk memulihkan proses yang sedang berjalan.
Try-Catch Manual
Try-catch manual memberikan kontrol penuh pada titik tertentu: Anda bisa membatalkan transaksi, mengirim fallback response, atau memetakan exception ke domain-specific error. Perlu diingat untuk tidak menambahkan try-catch hanya demi "menghindari error"—tanpa tindakan korektif, lebih baik biarkan handler global bekerja.
Contoh: Try-Catch di Controller
<?php
namespace App\Http\Controllers;
use App\Services\InvoiceService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
class InvoiceController extends Controller
{
public function store(Request $request, InvoiceService $service): JsonResponse
{
try {
$validated = $request->validate([
'customer_id' => 'required|exists:customers,id',
'items' => 'required|array|min:1'
]);
$invoice = $service->createInvoice($validated);
return response()->json([
'data' => $invoice,
'message' => 'Invoice berhasil dibuat'
], 201);
} catch (ValidationException $e) {
throw $e; // biarkan handler global men-generate response 422 standar
} catch (DomainException $e) {
return response()->json([
'error' => $e->getMessage()
], 400);
} catch (Throwable $e) {
report($e);
return response()->json([
'error' => 'Terjadi kegagalan di sisi server'
], 500);
}
}
}
Controller di atas memprioritaskan validasi, lalu menangani DomainException untuk skenario bisnis seperti stok kosong. Catch terakhir menggunakan Throwable dengan logging (report()) untuk memastikan error tak terduga tetap tercatat sebelum mengembalikan HTTP 500.
Try-Catch di Service Class
<?php
namespace App\Services;
use App\Exceptions\InventoryUnavailableException;
use App\Models\Invoice;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Throwable;
class InvoiceService
{
public function createInvoice(array $data): Invoice
{
try {
// misal memanggil service lain
$this->ensureInventory($data['items']);
return Invoice::create([
'number' => Str::uuid(),
'customer_id' => $data['customer_id'],
'payload' => $data['items']
]);
} catch (InventoryUnavailableException $e) {
Log::warning('Stok kosong saat membuat invoice', [
'customer_id' => $data['customer_id'],
'message' => $e->getMessage()
]);
throw $e; // biarkan controller memutuskan response
} catch (Throwable $e) {
Log::error('Gagal membuat invoice', [
'customer_id' => $data['customer_id'],
'exception' => $e
]);
throw new \RuntimeException('Invoice tidak dapat dibuat sekarang', 0, $e);
}
}
}
Service class sebaiknya melempar ulang exception setelah logging. Dengan begitu, lapisan atas tahu bahwa proses gagal dan dapat mengambil keputusan (rollback, response error, dsb). Membungkus Throwable menjadi exception baru menjaga agar detail internal tidak bocor sekaligus mempertahankan stack trace melalui parameter ketiga konstruktor.
DB::transaction() dengan Exception
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
DB::transaction(function () use ($payload) {
try {
$order = Order::create($payload['order']);
$order->items()->createMany($payload['items']);
event(new OrderPlaced($order));
} catch (Throwable $e) {
Log::error('Gagal menempatkan order', ['payload' => $payload, 'exception' => $e]);
throw $e; // penting agar transaksi di-rollback otomatis
}
});
Blok DB::transaction() akan otomatis melakukan rollback ketika closure melempar exception. Karena itu, setelah logging Anda wajib melempar ulang agar rollback tetap terjadi. Menelan exception di dalam transaksi adalah kesalahan umum yang membuat database tidak konsisten.
Logging dengan Log::error()
Gunakan Log::error() atau report() untuk mencatat konteks. Sertakan data penting namun hindari informasi sensitif.
try {
$response = $client->post('/api/payment', $payload);
} catch (Throwable $e) {
Log::error('Payment gateway error', [
'payload_hash' => sha1(json_encode($payload)),
'exception' => $e
]);
throw;
}
Menggunakan throw; tanpa argumen menjaga tipe exception asli. Logging kritikal sebaiknya terjadi sebelum rethrow agar tim observability mendapatkan sinyal dini.
Custom Exception di Laravel
Laravel memudahkan pembuatan exception khusus dengan php artisan make:exception. Custom exception berguna ketika domain bisnis membutuhkan sinyal spesifik yang tidak dapat diwakili exception bawaan.
<?php
namespace App\Exceptions;
use Exception;
class InventoryUnavailableException extends Exception
{
public function __construct(string $sku, int $requested)
{
parent::__construct("Stok {$sku} tidak cukup untuk {$requested} unit.");
}
}
Kapan melempar custom exception?
- Saat ingin membedakan business rule tertentu (misal batas saldo, kuota rate limit internal).
- Ketika lapisan atas perlu bereaksi berbeda: misalnya controller mengembalikan HTTP 409 untuk konflik stok.
- Untuk meningkatkan keterbacaan log dengan nama exception yang deskriptif.
Kapan tidak perlu?
- Jika exception bawaan sudah cukup (misal
ModelNotFoundException,AuthorizationException). - Jika hanya ingin mengganti pesan error tanpa perilaku berbeda.
- Jika custom exception tidak membawa konteks tambahan sehingga hanya memperumit kode.
Kapan Menggunakan Try-Catch?
- Boundary I/O: controller API, job, listener, atau command CLI yang memerlukan response terkontrol.
- Operasi kritis: transaksi database, integrasi pihak ketiga, pengiriman email penting.
- Graceful degradation: fallback cache, queue yang tidak boleh menghentikan sistem.
Kapan menghindari try-catch berlebihan?
- Jangan melingkupi seluruh method hanya untuk "menghindari error" tanpa penanganan spesifik.
- Biarkan exception mengalir ke handler global jika Anda tidak bisa mengambil tindakan yang lebih informatif.
- Hindari nested try-catch yang membuat alur sulit diikuti; pertimbangkan refactor ke service atau helper.
Kapan Menangkap Exception vs Throwable?
- Catch Exception: default choice pada controller/service karena mayoritas error Laravel adalah turunan
Exception. Ini menjaga agar error fatal sepertiTypeErrortetap terlihat selama pengujian. - Catch Throwable: gunakan di lapisan yang tidak boleh crash total (misal queue worker). Setelah logging, pertimbangkan rethrow atau
fail()agar job ditandai gagal. Jangan menelanThrowabledi code path biasa karena bisa menyembunyikan bug.
Kapan Harus Rethrow Setelah Logging?
Sebagai aturan umum, jika error membuat proses tidak valid atau data tidak konsisten, Anda harus melempar ulang setelah logging. Contohnya:
- Transaksi database yang harus rollback.
- Job queue yang perlu diulang atau ditandai gagal.
- Service layer yang ingin memberi tahu controller bahwa operasi gagal.
Anda boleh tidak melempar ulang hanya jika memiliki tindakan pemulihan lengkap (misal fallback cache sukses) dan data tetap konsisten.
Kesalahan Umum dan Tips Debugging
- Menelan exception: try-catch kosong atau hanya logging tanpa rethrow menyebabkan bug silent. Pastikan pengujian memeriksa jalur error.
- Double handling: menangani error yang sama di controller dan service secara redundan. Tentukan batas tanggung jawab.
- Tidak memanfaatkan handler global: jangan duplikasi logika format response 500 di tiap controller; gunakan handler untuk default.
- Kurang konteks log: tambahkan identifier seperti
request_id,user_id, atau hash payload agar mudah ditelusuri. - Tidak membedakan environment: di lokal, izinkan
APP_DEBUG=trueuntuk stack trace; di produksi gunakan logging terstruktur dan monitoring.
Untuk debugging, manfaatkan php artisan tinker untuk mereplikasi exception secara manual, serta gunakan php artisan queue:failed untuk menganalisis job yang gagal akibat exception.
Penutup
Try-catch di Laravel bukan sekadar alat untuk “menghilangkan error”, melainkan mekanisme kontrol alur bisnis. Pahami perbedaan Exception vs Throwable, pilih titik tangkap yang tepat, gunakan custom exception ketika logika domain memerlukan, dan selalu log dengan konteks yang jelas. Dengan pendekatan ini, aplikasi lebih mudah dipantau, mudah diuji, dan tetap tangguh menghadapi skenario gagal yang tidak terelakkan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!