This course is still being released! Check back later for more chapters.
Enhancing API Error Handling
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeHead over to src/Store/LemonSqueezyApi.php
. You probably remember this createCheckoutUrl()
method from earlier. This cast to string
fixed an error.
// ... 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
.
// ... 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
.
// ... 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.
// ... 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.
// ... 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.
// ... 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.
// ... 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.
// ... 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.