Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

ndeblauw's avatar

Not understanding specific exception logic

Hi all, I'm a bit puzzled by an Undefined variable $data arror that is thrown once I reach the foreach loop, after the following try-catch block in a job:

try {
  [$data, $meta] = function_that_fetches_data_from_external_api();
} catch (\Exception $exception) {
  $this->fail($exception);
}

foreach ($data as $item) {      // <------ here error is occuring (sometimes)
  // processing of the data
}

I assumed (I guess, wrongly) that either the data (and metadata) was fetched, and the processing would be possible, or that the job would fail, and the foreach loop would never be reached.

Can somebody help me explain in what (rare?) cases the try-catch block is executed successfully, yet without the creation of a $data variable?

0 likes
8 replies
martinbean's avatar

@ndeblauw Maybe try dd-ing the $data and $meta variables’ values? Give we have absolutely no idea what function_that_fetches_data_from_external_api does or returns.

ndeblauw's avatar

@martinbean, thanks for your follow up question. It returns an array or throws an exception if it cannot return data. It's dififcult to do dd-ing, as the error does only show up sporadically (in less than 0.5% of the cases)

public function function_that_fetches_data_from_external_api()(): array
{
    $url = $this->url.'?'.http_build_query($this->parameters);

    // Connect with the API or throw error/exception
    try {
        $response = Http::withToken($token)->withHeaders([
            'Content-Type' => 'application/json',
            'accept' => 'application/json',
        ])->get($url);
    } catch (\Exception $e) {
        throw new \Exception("No response was received from the API for url: $url");
    } catch (\Error $e) {
        throw new \Exception("No response was received from the  API for url: $url");
    }

    // Check for faulty respons
    if ($response->status() === 404 && json_decode($response->body())->metadata->totalRecords === 0) {
        return [json_decode('{}'), json_decode($response)->metadata];
    }

    // Check for erroneous response
    if ($response->status() != 200) {
        // ACTION: reset the token 
        throw new \Exception('Error getting data from the API');
    }

    // Success: return array
    return [json_decode($response)->data, json_decode($response)->metadata];
}
martinbean's avatar

@ndeblauw I think you’re over-complicating your code to be honest. Laravel’s HTTP client has a throws method to automatically throw exceptions if the request fails. Just catching these exceptions, and then converting them to plain Exception instances, just loses importing context that would help you in debugging any failed requests. The message "No response was received from the API for url: $url" isn’t really helpful as you’ve thrown away context such as the request details, the actual response that was returned, etc.

I’d just make the request and return the data if the request was successful:

public function function_that_fetches_data_from_external_api(): array
{
    $url = $this->url . '?' . http_build_query($this->parameters);

    $response = Http::withToken($token)
        ->acceptJson()
        ->get($url)
        ->throwUnlessStatus(200);

    return $response->json();
}

The method then just returns the response data if the request was successful, or exceptions are thrown if the request fails.

You’re also clearly not showing us your actual code given you had a typo (two ()() at the end of your method definition), and referencing a random $token variable that isn’t actually defined in the method scope.

ndeblauw's avatar

@martinbean Yes, I abstracted away some of the more complex aspects (how token is fetched) to make the code more readable. And I changed the name of the function to not disclose the name of the service.

My function will always have to return a [$data, $metadata] tuple (as this is used in many other places in the application), taken from the body of the api response. But I understand your suggestion, and will refactor that function in the spirit of your answer. Thanks for having taken the time!

1 like
Snapey's avatar

Also, you don't show what $this->fail($exception) does.

If this just swallows the error and returns, then your foreach will be run but without $data ever being set.

ndeblauw's avatar

@Snapey it is the fail() function from InteractsWithQueue for a job

    /**
     * Fail the job from the queue.
     *
     * @param  \Throwable|string|null  $exception
     * @return void
     */
    public function fail($exception = null) 
    {
        //...
     }
martinbean's avatar

@ndeblauw You don’t need to manually catch exceptions and call that fail method, though. The job will automatically be marked as failed if an exception is thrown within the handle method.

All you’re doing by catching exceptions, and re-throwing your own generic exception, is making your life harder by discarding any helpful context that would have been present in the original exception. Your failed_jobs table is going to go from display actual exceptions to just unhelpful “Error getting data” messages with no other information to assist you with debugging.

ndeblauw's avatar

I know it is an old thread, but still want to thank you guys (@martinbean and @snapey) for the different pieces of advice you have given. It did bring me back to the documentation and I refactored the whole function in the queued job.

1 like

Please or to participate in this conversation.