This course is still being released! Check back later for more chapters.

Get Notified About this Course!

We will send you messages regarding this course only
and nothing else, we promise.
You can unsubscribe anytime by emailing us at:
privacy@symfonycasts.com
Login to bookmark this video
Buy Access to Course
15.

Enhancing API Error Handling

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Head over to src/Store/LemonSqueezyApi.php. You probably remember this createCheckoutUrl() method from earlier. This cast to string fixed an error.

127 lines | src/Store/LemonSqueezyApi.php
// ... lines 1 - 11
final readonly class LemonSqueezyApi
{
// ... lines 14 - 23
public function createCheckoutUrl(User $user): string
{
// ... lines 26 - 35
$attributes['checkout_data']['custom']['user_id'] = $user->getId();
// ... lines 37 - 86
}
// ... lines 88 - 125
}

Remove it temporarily so we can bring that error back. Back in your browser, click "Add to cart", then "Checkout with LemonSqueezy", and... we see our expected ClientException.

Previously, we used this dd($response->getContent(false)) trick to see the details behind the ClientException. Uncomment this line, refresh the page, and... now we can see the actual error.

Making Error Messages More Informative

This is okay, but I bet we could make it even better. Instead of using dd() to debug, let's try wrapping the client->request() method in another method. At the bottom of this class, create a private function called request() that returns an array.

131 lines | src/Store/LemonSqueezyApi.php
// ... lines 1 - 126
private function request(string $method, string $url, array $options = []): array
{
}
// ... lines 130 - 131

Its first argument will be string $method, followed by string $url and an array of options. Now here's the fun part: Open a try-catch block and, in the try, write $response = $this->client->request() and pass in all the variables - $method, $url, and $options. Create a $data variable that's equal to $response->toArray(). We'll catch ClientException $e.

140 lines | src/Store/LemonSqueezyApi.php
// ... lines 1 - 127
private function request(string $method, string $url, array $options = []): array
{
try {
$response = $this->client->request($method, $url, $options);
$data = $response->toArray();
} catch (ClientException $e) {
}
// ... lines 136 - 137
}
// ... lines 139 - 140

At the bottom, return $data, and back in the catch, we want the raw response content, so write $data = $e->getResponse()->toArray() and pass false as the first argument. We'll also add dd($data) here temporarily so we can see the API error response.

141 lines | src/Store/LemonSqueezyApi.php
// ... lines 1 - 127
private function request(string $method, string $url, array $options = []): array
{
try {
// ... lines 131 - 132
} catch (ClientException $e) {
$data = $e->getResponse()->toArray(false);
dd($data);
}
return $data;
}
// ... lines 140 - 141

Next, update the createCheckoutUrl() method. Instead of $this->client->request(), use just $this->request(), passing all the same arguments. If we head over and try to check out again... boom! This is a proper dump of the real API request as an array.

141 lines | src/Store/LemonSqueezyApi.php
// ... lines 1 - 24
public function createCheckoutUrl(User $user): string
{
// ... lines 27 - 61
$response = $this->request(Request::METHOD_POST, 'checkouts', [
// ... lines 63 - 82
]);
// ... lines 84 - 87
}
// ... lines 89 - 141

Crafting Helpful Error Messages

Okay, instead of "dumping and dying", let's craft some error messages that are more helpful. In our code, find the request() method. Comment out this dd() statement and below, add $mainErrorMessage = 'LS API Error:'. Now, let's check if we have an error with $error = $data['errors'][0] ?? null. If there is an error, do another check with if (isset($error['status']). Inside, write $mainErrorMessage .= ' ' . $error['status']. Do the same for title, detail, and source.pointer. I'll speed through this part. Finally, else, and inside, append the raw content with $mainErrorMessage .= $e->getResponse()->getContent(false). Perfect!

At the end, throw new \Exception() with $mainErrorMessage, 0 for the second argument, and $e as the third argument.

163 lines | src/Store/LemonSqueezyApi.php
// ... lines 1 - 127
private function request(string $method, string $url, array $options = []): array
{
try {
// ... lines 131 - 132
} catch (ClientException $e) {
// ... lines 134 - 136
$mainErrorMessage = 'LS API Error:';
$error = $data['errors'][0] ?? null;
if ($error) {
if (isset($error['status'])) {
$mainErrorMessage .= ' ' . $error['status'];
}
if (isset($error['title'])) {
$mainErrorMessage .= ' ' . $error['title'];
}
if (isset($error['detail'])) {
$mainErrorMessage .= ' "' . $error['detail'] . '"';
}
if (isset($error['source']['pointer'])) {
$mainErrorMessage .= sprintf(' (at path "%s")', $error['source']['pointer']);
}
} else {
$mainErrorMessage .= $e->getResponse()->getContent(false);
}
throw new \Exception($mainErrorMessage, 0, $e);
}
// ... lines 159 - 160
}
// ... lines 162 - 163

This sets the original exception as the previous one, which further helps with debugging. That's it! This is a fairly common and useful pattern for simplifying complex exceptions but still providing a reference to the original.

Let's give it a try!

Over on the checkout page, refresh, and... voila! The generic error message is now a custom message:

LS API Error: 422 Unprocessable entity "The {0} field must be a string" (at path "data/attributes/checkout_data/custom/user_id").

That was much easier to understand.

All we need to do now is return the string typecasting on the user_id line. We don't need this $response->toArray() line anymore, so we can delete that along with the dd(). Also replace the $response variable with $lsCheckout, since we already have an array of checkout object data here.

161 lines | src/Store/LemonSqueezyApi.php
// ... lines 1 - 24
public function createCheckoutUrl(User $user): string
{
// ... lines 27 - 36
$attributes['checkout_data']['custom']['user_id'] = (string) $user->getId();
// ... lines 38 - 61
$lsCheckout = $this->request(Request::METHOD_POST, 'checkouts', [
// ... lines 63 - 82
]);
// ... lines 84 - 85
}
// ... lines 87 - 161

Refresh the page again to see if it works, and... we're good!

The final step is to replace all remaining $this->client->request() calls with $this->request(). I'll speed through this for retrieveStoreUrl(), listOrders(), and remove the $response->toArray() calls while I'm at it.

156 lines | src/Store/LemonSqueezyApi.php
// ... lines 1 - 12
final readonly class LemonSqueezyApi
{
// ... lines 15 - 87
public function retrieveStoreUrl(): string
{
$lsStore = $this->request(Request::METHOD_GET, 'stores/' . $this->storeId);
return $lsStore['data']['attributes']['url'];
}
// ... line 94
public function listOrders(User $user): array
{
// ... lines 97 - 102
return $this->request(Request::METHOD_GET, 'orders', [
// ... lines 104 - 112
]);
}
public function retrieveCustomer(string $customerId): array
{
return $this->request(Request::METHOD_GET, 'customers/' . $customerId);
}
// ... lines 120 - 154
}

If we try our site one more time... the account page still works... and so does the checkout page! Our error handling process is now efficient and informative.

Next: Let's improve our customers' checkout experience by embedding the LemonSqueezy checkout page in our app.