Laravel Patches provides comprehensive error handling to ensure patches fail gracefully and provide detailed information for troubleshooting.
Error Behavior
Default Behavior
By default, if a patch throws an exception:
- Execution stops immediately
- Error is logged to the patches table
- Exception is re-thrown
- No subsequent patches run
Continue on Error
You can configure patches to continue running even when one fails:
1// config/laravel-patches.php2return [3 'stop_on_error' => false, // Continue running patches after failures4];
Or use environment variable:
1PATCHES_STOP_ON_ERROR=false
Rollback Failures
If a patch's down() method throws, rollback stops and rethrows the exception. The patch's database record is retained so you can correct the rollback and retry it. stop_on_error applies to running patches; rollback always stops on failure to preserve rollback order.
PatchFailed is dispatched with the original batch on failure. PatchRolledBack is dispatched only when the rollback succeeds. Enable transactions if the database changes in a failed down() method should also be rolled back.
Error Logging
Automatic Error Capture
When a patch fails, the following information is automatically captured:
- Error message
- Stack trace
- Execution time before failure
- Memory usage
- User who ran the patch
- Environment (production, staging, etc.)
Database Storage
Failed patches are stored in the patches table with:
1[2 'status' => 'failed',3 'error_message' => 'Exception message',4 'error_trace' => 'Full stack trace',5 'execution_time_ms' => 1234,6]
Viewing Failed Patches
1# See all patches including failed ones2php artisan patch:status3 4# Filter only failed patches5php artisan patch:list --status=failed
Configuration
config/laravel-patches.php
1return [ 2 /** 3 * Stop execution on first error 4 */ 5 'stop_on_error' => env('PATCHES_STOP_ON_ERROR', true), 6 7 /** 8 * Log errors to patches table 9 */10 'log_errors' => env('PATCHES_LOG_ERRORS', true),11];
Handling Errors in Patches
Try-Catch Blocks
Handle specific errors within your patch:
1use Rappasoft\LaravelPatches\Patch; 2 3class MyPatch extends Patch 4{ 5 public function up() 6 { 7 try { 8 // Risky operation 9 User::where('email', 'LIKE', '%old-domain.com')10 ->update(['email' => DB::raw("REPLACE(email, 'old-domain.com', 'new-domain.com')")]);11 12 $this->log('Updated user emails');13 } catch (\Exception $e) {14 $this->log('Error updating emails: ' . $e->getMessage());15 16 // Optionally rethrow17 throw $e;18 }19 }20 21 public function down()22 {23 // Rollback logic24 }25}
Validation Before Execution
Validate data before making changes:
1public function up() 2{ 3 // Check if operation is safe 4 if (User::whereNull('email')->exists()) { 5 throw new \Exception('Cannot proceed: Users with null emails exist'); 6 } 7 8 // Proceed with patch 9 // ...10}
Progressive Error Handling
1public function up() 2{ 3 $errors = []; 4 $processed = 0; 5 6 User::chunk(100, function ($users) use (&$errors, &$processed) { 7 foreach ($users as $user) { 8 try { 9 $user->update(['status' => 'active']);10 $processed++;11 } catch (\Exception $e) {12 $errors[] = "User {$user->id}: {$e->getMessage()}";13 }14 }15 });16 17 $this->log("Processed: {$processed} users");18 19 if (count($errors)) {20 $this->log("Errors: " . implode(', ', $errors));21 throw new \Exception(count($errors) . ' users failed to update');22 }23}
Event-Based Error Handling
Subscribe to the PatchFailed event for custom error handling:
1use Rappasoft\LaravelPatches\Events\PatchFailed; 2 3Event::listen(PatchFailed::class, function (PatchFailed $event) { 4 // Send alert 5 Notification::send( 6 User::admins()->get(), 7 new PatchFailedNotification($event->patch, $event->exception) 8 ); 9 10 // Log to external service11 Bugsnag::notifyException($event->exception, [12 'patch' => $event->patch,13 'batch' => $event->batch,14 ]);15 16 // Create rollback plan17 DB::table('patch_failures')->insert([18 'patch' => $event->patch,19 'error' => $event->exception->getMessage(),20 'needs_manual_intervention' => true,21 'created_at' => now(),22 ]);23});
Best Practices
1. Use Descriptive Error Messages
1throw new \Exception("Failed to update user #{$user->id}: email validation failed");
2. Log Progress
1$this->log("Processing 1000 records...");2// Process records3$this->log("Completed successfully");
3. Validate Preconditions
1public function up()2{3 if (! Schema::hasColumn('users', 'old_field')) {4 throw new \Exception('old_field column does not exist');5 }6 7 // Proceed with migration8}
4. Use Transactions (see Transactions documentation)
1protected bool $useTransaction = true;
5. Test Error Scenarios
1// In your tests2test('handles duplicate email error', function () {4 5 $this->expectException(\Exception::class);6 7 Artisan::call('patch');8});
Debugging Failed Patches
1. Check Logs
1# Laravel logs2tail -f storage/logs/laravel.log
2. Query Failed Patches
1use Rappasoft\LaravelPatches\Models\Patch;2 3$failed = Patch::where('status', 'failed')->get();4 5foreach ($failed as $patch) {6 dump($patch->patch);7 dump($patch->error_message);8 dump($patch->error_trace);9}
3. Re-run Individual Patch
Create a temporary command to re-run a specific patch:
1// app/Console/Commands/RerunPatch.php 2public function handle() 3{ 4 $patchName = $this->argument('patch'); 5 6 // Delete old failed attempt 7 Patch::where('patch', $patchName)->delete(); 8 9 // Re-run the patch10 Artisan::call('patch');11}
Recovery Strategies
After a Failed Patch
- Review the error in
patch:statusor database - Understand the cause from error message and trace
- Fix the data or code that caused the failure
- Rollback if needed with
patch:rollback - Fix the patch file if it contains bugs
- Re-run patches with
php artisan patch
Manual Intervention
If a patch partially completed before failing:
1public function up() 2{ 3 // Check what was already done 4 if (DB::table('temp_migration_state')->exists()) { 5 $this->log('Resuming from previous failed attempt'); 6 // Continue from checkpoint 7 } else { 8 // Start fresh 9 }10 11 // Your patch logic with checkpoints12}
Production Safeguards
1. Always Test First
1# Test in staging2php artisan patch --dry-run3php artisan patch
2. Backup Before Running
1php artisan snapshot:create2php artisan patch
3. Monitor Execution
1# Run with verbose output2php artisan patch -v
4. Have Rollback Plan
Ensure your down() methods can properly rollback:
1public function down()2{3 // Reverse the changes from up()4}