How to fix Laravel Queue's "ModelNotFoundException"

What to do when your Laravel queued job fails due to the relevant models not being found? Let's examine your options of handling ModelNotFoundException in a queued job.

Computer screen showing the number "404" on it
Photo by Erik Mclean / Unsplash
Illuminate\Database\Eloquent\ModelNotFoundException: No query results for model [App\Models\User]

This is a very common error, especially on larger scale projects when lots of jobs are being processed and unexpected things happens. There's a couple main reasons why this can happen:

  1. You did not save the model to the database before dispatching the job
  2. The model was deleted between job dispatch and job execution
  3. The job/listener was dispatched within a database transaction that was later rolled back

Luckily all three reasons are easy to fix.

Saving models before job dispatch

Make sure you understand the difference between Model::create(), Model::make(), and new Model in your application.

$model = new Model creates a new model instance, but it still hasn't saved it to the database. Once you're finished assigning properties, make sure to call the ->save() method to save it to the database.

<?php

$user = new User;
$user->name = 'Arunas';
// At this point, the model is not saved yet.

// Make sure to call the ->save() method to save it to the database
$user->save();

The same applies when creating instances through Model::make():

<?php

$user = User::make(['name' => 'Arunas']);
// at this point, the model is not saved yet.

// Make sure to call the ->save() method to save it to the database
$user->save();

Model was deleted between job dispatch and job execution

Unless your queue driver is set to sync, your queued jobs will be executed at some point in the future. A lot can happen in that time, even if it's just one second. A database transaction rollback, an exception, or something else that would cause your model to get deleted. And by the time your queue worker picks up the job, the model is all gone and the job fails with a ModelNotFoundException.

In that case, you have two options to handle this.

a. Ignore the job when the models are missing

This can often be the desired behaviour. If, for example, you have a job for sending a welcome email to a new user, but the user registration fails - you don't want that email to be sent.

You can achieve this by adding a public $deleteWhenMissingModels = true; property to the job (Laravel 5.7+):

<?php

class SendWelcomeEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    /**
     * @var User
     */
    public $user;

    /**
     * Delete the job if its models no longer exist.
     *
     * @var bool
     */
    public $deleteWhenMissingModels = true;
    
    ...
}

b. Don't serialize models and store whole instances on the worker queue

This way, the whole object is stored as-is in the worker queue. When the job is picked up by the worker, it doesn't need to load the model from the database any more because it already has it!

To do this, simply remove the SeralizesModels trait from the job:

<?php

class SendWelcomeEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable;
    
    /**
     * @var User
     */
    public $user;
    
    ...
}

Delaying jobs / listeners until after a database transaction is committed

If you're ever dispatching jobs or events within a database transaction, you're running the risk of the transaction rolling back and the queued jobs not having any data to work with. You can learn more about delaying jobs & listeners here, but I'll give you a quick fix in this article as well.

Dispatching a job after transaction is committed

There are a few ways to do this, but here's a couple useful ideas.

Use the ->afterCommit() method when dispatching a job:

DB::transaction(function () {
    // Perform database queries here

    dispatch(new MyJob())->afterCommit();
    
    // alternatively, if the job uses the Dispatchable trait:
    // MyJob::dispatch($data)->afterCommit();

    // Perform other operations that could potentially fail
    // and roll back the transaction.
});

Or, you could use the DB::afterCommit() hook to run a function when the transaction is committed:

DB::transaction(function () {
    // Perform database queries here

    DB::afterCommit(function () {
        dispatch(new MyJob($data));
    });

    // Perform other operations that could potentially fail
    // and roll back the transaction.
});

The DB::afterCommit() option is great if you have multiple statements to execute, not just dispatching a job.

Delaying event listener until after the transaction is committed

Add a $afterCommit = true property to the event listener and it will be run only after the transaction is complete. It does not matter whether it is synchronous or asynchronous (implements the ShouldQueue interface).

class SendNotificationListener
{
    public $afterCommit = true;

    public function handle(MyEvent $event)
    {
        // Send notification email here
    }
}