Add form value persistence across cache revalidation re-renders 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
246 lines
5.9 KiB
Markdown
Executable File
246 lines
5.9 KiB
Markdown
Executable File
---
|
|
name: ajax-error-handling
|
|
description: Handling Ajax errors in RSX including response formats, error codes, client-side error display, and Form_Utils. Use when implementing error handling for Ajax calls, working with response_error(), form validation errors, or debugging Ajax failures.
|
|
---
|
|
|
|
# RSX Ajax Error Handling
|
|
|
|
## Response Architecture
|
|
|
|
RSX returns **HTTP 200 for ALL Ajax responses** (success and errors). Success/failure is encoded in the response body via `_success` field.
|
|
|
|
```javascript
|
|
// Success response
|
|
{
|
|
"_success": true,
|
|
"_ajax_return_value": { /* your data */ }
|
|
}
|
|
|
|
// Error response
|
|
{
|
|
"_success": false,
|
|
"error_code": "validation|not_found|unauthorized|auth_required|fatal",
|
|
"reason": "User-friendly message",
|
|
"metadata": { /* field errors for validation */ }
|
|
}
|
|
```
|
|
|
|
**Rationale**: Batch requests need uniform status codes. Always get parseable response body. Non-200 only means "couldn't reach PHP at all".
|
|
|
|
---
|
|
|
|
## Error Codes
|
|
|
|
| Constant | Purpose |
|
|
|----------|---------|
|
|
| `Ajax::ERROR_VALIDATION` | Field validation failures |
|
|
| `Ajax::ERROR_NOT_FOUND` | Resource not found |
|
|
| `Ajax::ERROR_UNAUTHORIZED` | User lacks permission |
|
|
| `Ajax::ERROR_AUTH_REQUIRED` | User not logged in |
|
|
| `Ajax::ERROR_FATAL` | Uncaught PHP exceptions |
|
|
|
|
Constants available in both PHP (`Ajax::ERROR_*`) and JavaScript (`Ajax.ERROR_*`).
|
|
|
|
---
|
|
|
|
## Server-Side: Returning Errors
|
|
|
|
Use `response_error()` helper - never return `_success` manually:
|
|
|
|
```php
|
|
#[Ajax_Endpoint]
|
|
public static function save(Request $request, array $params = []) {
|
|
// Validation error with field-specific messages
|
|
if (empty($params['email'])) {
|
|
return response_error(Ajax::ERROR_VALIDATION, [
|
|
'email' => 'Email is required'
|
|
]);
|
|
}
|
|
|
|
// Not found error
|
|
$user = User_Model::find($params['id']);
|
|
if (!$user) {
|
|
return response_error(Ajax::ERROR_NOT_FOUND, 'User not found');
|
|
}
|
|
|
|
// Success - just return data (framework wraps it)
|
|
$user->name = $params['name'];
|
|
$user->save();
|
|
return ['id' => $user->id];
|
|
}
|
|
```
|
|
|
|
### Let Exceptions Bubble
|
|
|
|
Don't wrap database/framework operations in try/catch. Let exceptions bubble to the global handler:
|
|
|
|
```php
|
|
// WRONG - Don't catch framework exceptions
|
|
try {
|
|
$user->save();
|
|
} catch (Exception $e) {
|
|
return ['error' => $e->getMessage()];
|
|
}
|
|
|
|
// CORRECT - Let it throw
|
|
$user->save(); // Framework catches and formats
|
|
```
|
|
|
|
**When try/catch IS appropriate**: File uploads, external API calls, user input parsing (expected failures).
|
|
|
|
---
|
|
|
|
## Client-Side: Handling Errors
|
|
|
|
Ajax.js automatically unwraps responses:
|
|
- `_success: true` → Promise resolves with `_ajax_return_value`
|
|
- `_success: false` → Promise rejects with Error object
|
|
|
|
```javascript
|
|
try {
|
|
const user = await User_Controller.get_user(123);
|
|
console.log(user.name); // Already unwrapped
|
|
} catch (error) {
|
|
console.log(error.code); // 'validation', 'not_found', etc.
|
|
console.log(error.message); // User-displayable message
|
|
console.log(error.metadata); // Field errors for validation
|
|
}
|
|
```
|
|
|
|
### Automatic Error Display
|
|
|
|
Uncaught Ajax errors automatically display via `Modal.error()`:
|
|
|
|
```javascript
|
|
// No try/catch - error shows in modal automatically
|
|
const user = await User_Controller.get_user(123);
|
|
```
|
|
|
|
---
|
|
|
|
## Form Error Handling
|
|
|
|
### With Rsx_Form (Recommended)
|
|
|
|
```javascript
|
|
const result = await Modal.form({
|
|
title: 'Add User',
|
|
component: 'User_Form',
|
|
on_submit: async (form) => {
|
|
try {
|
|
const result = await Controller.save(form.vals());
|
|
return result; // Success - close modal
|
|
} catch (error) {
|
|
await form.render_error(error);
|
|
return false; // Keep modal open
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
`form.render_error()` handles all error types:
|
|
- **validation**: Shows inline field errors + alert for unmatched errors
|
|
- **fatal/network/auth**: Shows error in form's error container
|
|
|
|
### With Form_Utils (Non-Rsx_Form)
|
|
|
|
```javascript
|
|
try {
|
|
const result = await Controller.save(form_data);
|
|
} catch (error) {
|
|
if (error.code === Ajax.ERROR_VALIDATION) {
|
|
Form_Utils.apply_form_errors($form, error.metadata);
|
|
} else {
|
|
Rsx.render_error(error, '#error_container');
|
|
}
|
|
}
|
|
```
|
|
|
|
`Form_Utils.apply_form_errors()`:
|
|
- Matches errors to fields by `name` attribute
|
|
- Adds `.is-invalid` class and inline error text
|
|
- Shows alert ONLY for errors that couldn't match to fields
|
|
|
|
---
|
|
|
|
## Error Display Methods
|
|
|
|
### Modal.error() - Critical Errors
|
|
|
|
```javascript
|
|
await Modal.error(error, 'Operation Failed');
|
|
```
|
|
|
|
Red danger modal, can stack over other modals.
|
|
|
|
### Rsx.render_error() - Container Display
|
|
|
|
```javascript
|
|
Rsx.render_error(error, '#error_container');
|
|
Rsx.render_error(error, this.$sid('error'));
|
|
```
|
|
|
|
Displays error in any container element.
|
|
|
|
---
|
|
|
|
## Developer vs Production Mode
|
|
|
|
**Developer Mode** (`IS_DEVELOPER=true`):
|
|
- Full exception message
|
|
- File path and line number
|
|
- SQL queries with parameters
|
|
- Stack trace (up to 10 frames)
|
|
|
|
**Production Mode**:
|
|
- Generic message: "An unexpected error occurred"
|
|
- No technical details exposed
|
|
- Errors logged server-side
|
|
|
|
---
|
|
|
|
## Common Patterns
|
|
|
|
### Simple Validation
|
|
|
|
```php
|
|
#[Ajax_Endpoint]
|
|
public static function save(Request $request, array $params = []) {
|
|
$errors = [];
|
|
|
|
if (empty($params['email'])) {
|
|
$errors['email'] = 'Email is required';
|
|
}
|
|
if (empty($params['name'])) {
|
|
$errors['name'] = 'Name is required';
|
|
}
|
|
|
|
if ($errors) {
|
|
return response_error(Ajax::ERROR_VALIDATION, $errors);
|
|
}
|
|
|
|
// ... save logic
|
|
}
|
|
```
|
|
|
|
### Check Specific Error Type
|
|
|
|
```javascript
|
|
try {
|
|
const data = await Controller.get_data(id);
|
|
} catch (error) {
|
|
if (error.code === Ajax.ERROR_NOT_FOUND) {
|
|
show_not_found_message();
|
|
} else if (error.code === Ajax.ERROR_UNAUTHORIZED) {
|
|
redirect_to_login();
|
|
} else {
|
|
// Let default handler show modal
|
|
throw error;
|
|
}
|
|
}
|
|
```
|
|
|
|
## More Information
|
|
|
|
Details: `php artisan rsx:man ajax_error_handling`
|