# LiveComponents Error Recovery Strategy ## Error Categories ### 1. Component Errors - Unknown component name - Invalid component ID format - Component instantiation failure - Missing dependencies ### 2. Action Errors - Unknown action method - Action method not callable - Invalid action parameters - Action execution exception ### 3. State Errors - Invalid state format - State deserialization failure - State validation error - State version conflict ### 4. Rendering Errors - Template not found - Template syntax error - Rendering exception - Cache write failure ### 5. Upload Errors - File validation failure - Upload size limit exceeded - Invalid file type - Storage write failure ### 6. Lifecycle Errors - onMount() exception - onUpdate() exception - onDestroy() exception ## Error Handling Strategy ### Server-Side Error Handling #### 1. Component Resolution Errors **Current Behavior:** ```php public function resolve(ComponentId $componentId, ?ComponentData $state = null): LiveComponentContract { if (!$className) { throw new \InvalidArgumentException("Unknown component: {$componentName}"); } } ``` **Improved Error Recovery:** ```php use App\Framework\Exception\FrameworkException; use App\Framework\Exception\ErrorCode; public function resolve(ComponentId $componentId, ?ComponentData $state = null): LiveComponentContract { [$componentName, $instanceId] = $this->parseComponentId($componentId->toString()); $className = $this->nameToClassMap[$componentName] ?? null; if (!$className) { // Find similar component names (Levenshtein distance) $suggestions = $this->findSimilarComponents($componentName); throw FrameworkException::create( ErrorCode::COMPONENT_NOT_FOUND, "Unknown component: '{$componentName}'" )->withData([ 'component_name' => $componentName, 'instance_id' => $instanceId, 'suggestions' => $suggestions, 'available_components' => array_keys($this->nameToClassMap) ]); } try { return $this->container->invoker->make($className, [ 'id' => $componentId, 'initialData' => $state ]); } catch (\Throwable $e) { throw FrameworkException::create( ErrorCode::COMPONENT_INSTANTIATION_FAILED, "Failed to instantiate component '{$componentName}'" )->withData([ 'component_class' => $className, 'error' => $e->getMessage() ])->withPrevious($e); } } private function findSimilarComponents(string $componentName): array { $suggestions = []; foreach (array_keys($this->nameToClassMap) as $availableName) { $distance = levenshtein($componentName, $availableName); if ($distance <= 3) { // Max 3 character difference $suggestions[] = $availableName; } } return $suggestions; } ``` #### 2. Action Execution Errors **Enhanced LiveComponentHandler:** ```php public function handle( LiveComponentContract $component, string $actionName, ActionParameters $params ): ComponentUpdate { // Validate action exists if (!method_exists($component, $actionName)) { $availableActions = $this->getAvailableActions($component); throw FrameworkException::create( ErrorCode::ACTION_NOT_FOUND, "Unknown action '{$actionName}' on component " . get_class($component) )->withData([ 'action_name' => $actionName, 'available_actions' => $availableActions, 'component_class' => get_class($component) ]); } // Validate action is public and non-static $reflection = new \ReflectionMethod($component, $actionName); if (!$reflection->isPublic() || $reflection->isStatic()) { throw FrameworkException::create( ErrorCode::ACTION_NOT_CALLABLE, "Action '{$actionName}' is not a public instance method" )->withData([ 'action_name' => $actionName, 'is_public' => $reflection->isPublic(), 'is_static' => $reflection->isStatic() ]); } // Execute with error recovery try { $result = $this->container->invoker->invoke($component, $actionName, $params->toArray()); // Validate result type if (!$result instanceof ComponentData) { throw FrameworkException::create( ErrorCode::ACTION_INVALID_RETURN, "Action '{$actionName}' must return ComponentData" )->withData([ 'action_name' => $actionName, 'returned_type' => get_debug_type($result) ]); } return ComponentUpdate::fromActionResult($component, $result); } catch (FrameworkException $e) { // Re-throw framework exceptions throw $e; } catch (\Throwable $e) { // Wrap other exceptions throw FrameworkException::create( ErrorCode::ACTION_EXECUTION_FAILED, "Action '{$actionName}' execution failed" )->withData([ 'action_name' => $actionName, 'component_class' => get_class($component), 'error_message' => $e->getMessage(), 'error_type' => get_class($e) ])->withPrevious($e); } } private function getAvailableActions(LiveComponentContract $component): array { $reflection = new \ReflectionClass($component); $actions = []; foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { if ($method->isStatic() || $method->isConstructor()) { continue; } // Skip getter/setter/lifecycle methods if (in_array($method->getName(), ['getId', 'getData', 'getRenderData', 'onMount', 'onUpdate', 'onDestroy'])) { continue; } // Check return type $returnType = $method->getReturnType(); if ($returnType instanceof \ReflectionNamedType && $returnType->getName() === ComponentData::class) { $actions[] = $method->getName(); } } return $actions; } ``` #### 3. State Deserialization Errors **Safe State Handling:** ```php #[Route('/live-component/{id}', method: Method::POST)] public function handleAction(string $id, HttpRequest $request): JsonResult { try { $action = ComponentAction::fromRequest($request, $id); } catch (\Throwable $e) { return new JsonResult([ 'success' => false, 'error' => 'Invalid action format', 'details' => $e->getMessage() ], Status::BAD_REQUEST); } // Safe state deserialization try { $stateArray = $request->parsedBody?->get('state', []); if (!is_array($stateArray)) { throw new \InvalidArgumentException('State must be an array'); } $state = ComponentData::fromArray($stateArray); } catch (\Throwable $e) { return new JsonResult([ 'success' => false, 'error' => 'Invalid state format', 'details' => $e->getMessage(), 'recovery' => 'Client should reload component from server' ], Status::BAD_REQUEST); } // Component resolution with error recovery try { $component = $this->componentRegistry->resolve($action->componentId, $state); } catch (FrameworkException $e) { return new JsonResult([ 'success' => false, 'error' => $e->getMessage(), 'data' => $e->getData(), 'recovery' => 'Check component name and reload page' ], Status::NOT_FOUND); } // Action handling with error recovery try { $update = $this->handler->handle($component, $action->method, $action->params); } catch (FrameworkException $e) { return new JsonResult([ 'success' => false, 'error' => $e->getMessage(), 'data' => $e->getData(), 'recovery' => 'Try again or reload component' ], Status::UNPROCESSABLE_ENTITY); } catch (\Throwable $e) { // Unexpected errors error_log("LiveComponent action error: " . $e->getMessage()); return new JsonResult([ 'success' => false, 'error' => 'Internal server error', 'recovery' => 'Please reload the page', 'error_id' => uniqid('lc_error_') // For support ], Status::INTERNAL_SERVER_ERROR); } // Rendering with error recovery try { $updatedComponent = $this->componentRegistry->resolve( $action->componentId, ComponentData::fromArray($update->state->data) ); $html = $this->componentRegistry->render($updatedComponent); return new JsonResult([ 'success' => true, 'html' => $html, 'state' => $update->state->toArray(), 'events' => $update->events ]); } catch (\Throwable $e) { error_log("LiveComponent render error: " . $e->getMessage()); return new JsonResult([ 'success' => false, 'error' => 'Component rendering failed', 'state' => $update->state->toArray(), // Return state so client can retry 'recovery' => 'Try again', 'error_id' => uniqid('lc_render_') ], Status::INTERNAL_SERVER_ERROR); } } ``` #### 4. Lifecycle Hook Errors **Safe Lifecycle Hook Execution:** ```php // In LiveComponentHandler public function callMountHook(LiveComponentContract $component): void { if (!$component instanceof LifecycleAware) { return; } try { $component->onMount(); } catch (\Throwable $e) { // Log but don't fail - lifecycle hooks are side effects only error_log(sprintf( "Lifecycle hook onMount() failed for %s: %s", get_class($component), $e->getMessage() )); // Optional: Dispatch error event for monitoring $this->eventDispatcher?->dispatch( new ComponentLifecycleErrorEvent( componentId: $component->getId(), hook: 'onMount', error: $e ) ); } } private function callUpdateHook(LiveComponentContract $component): void { if (!$component instanceof LifecycleAware) { return; } try { $component->onUpdate(); } catch (\Throwable $e) { error_log(sprintf( "Lifecycle hook onUpdate() failed for %s: %s", get_class($component), $e->getMessage() )); } } ``` ### Client-Side Error Recovery #### JavaScript Error Handling **Enhanced LiveComponents Client:** ```javascript class LiveComponentManager { async handleAction(componentId, actionName, params = {}) { const component = this.components.get(componentId); if (!component) { console.error(`Component not found: ${componentId}`); return; } // Show loading state component.element.classList.add('lc-loading'); try { const response = await this.sendAction(componentId, actionName, params); if (!response.success) { this.handleError(component, response); return; } // Update component this.updateComponent(component, response); } catch (error) { this.handleNetworkError(component, error); } finally { component.element.classList.remove('lc-loading'); } } handleError(component, response) { const { error, recovery, data } = response; console.error(`Component error: ${error}`, data); // Show user-friendly error message this.showErrorMessage(component.element, error); // Implement recovery strategy switch (recovery) { case 'Try again': this.showRetryButton(component); break; case 'Client should reload component from server': this.reloadComponent(component); break; case 'Check component name and reload page': this.showReloadPageMessage(component); break; default: // Unknown recovery - show generic error this.showGenericError(component); } // Dispatch error event for custom handling component.element.dispatchEvent(new CustomEvent('lc:error', { detail: { error, recovery, data }, bubbles: true })); } handleNetworkError(component, error) { console.error('Network error:', error); // Retry logic with exponential backoff if (component.retryCount < 3) { const delay = Math.pow(2, component.retryCount) * 1000; component.retryCount++; setTimeout(() => { console.log(`Retrying action (attempt ${component.retryCount})...`); // Retry last action this.handleAction( component.id, component.lastAction, component.lastParams ); }, delay); } else { this.showErrorMessage( component.element, 'Network error. Please check your connection and reload the page.' ); } } reloadComponent(component) { // Reload component from server with current state fetch(`/live-component/${component.id}/reload`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ state: component.state }) }) .then(response => response.json()) .then(data => { if (data.success) { this.updateComponent(component, data); } else { this.showReloadPageMessage(component); } }) .catch(() => { this.showReloadPageMessage(component); }); } showRetryButton(component) { const retryHtml = `

Action failed. Would you like to try again?

`; component.element.insertAdjacentHTML('beforeend', retryHtml); } retry(componentId) { const component = this.components.get(componentId); if (component && component.lastAction) { this.handleAction( component.id, component.lastAction, component.lastParams ); } } } ``` ## Error Codes ### Component Errors (COMP_*) - `COMP_NOT_FOUND` - Component name not registered - `COMP_INSTANTIATION_FAILED` - Failed to create component instance - `COMP_INVALID_ID` - Invalid component ID format ### Action Errors (ACTION_*) - `ACTION_NOT_FOUND` - Action method does not exist - `ACTION_NOT_CALLABLE` - Action is not public or is static - `ACTION_INVALID_PARAMS` - Invalid action parameters - `ACTION_EXECUTION_FAILED` - Action threw exception - `ACTION_INVALID_RETURN` - Action did not return ComponentData ### State Errors (STATE_*) - `STATE_INVALID_FORMAT` - State is not valid JSON/array - `STATE_DESERIALIZATION_FAILED` - Failed to parse state - `STATE_VALIDATION_FAILED` - State validation error - `STATE_VERSION_CONFLICT` - Optimistic locking conflict ### Rendering Errors (RENDER_*) - `RENDER_TEMPLATE_NOT_FOUND` - Template file does not exist - `RENDER_TEMPLATE_SYNTAX` - Template has syntax error - `RENDER_EXCEPTION` - Rendering threw exception - `RENDER_CACHE_WRITE_FAILED` - Failed to write to cache ## Testing Error Recovery ### Unit Tests ```php it('suggests similar component names on not found error', function () { $registry = new ComponentRegistry(...); try { $registry->resolve(ComponentId::fromString('conter:test'), null); expect(false)->toBeTrue(); // Should have thrown } catch (FrameworkException $e) { expect($e->getData()['suggestions'])->toContain('counter'); } }); it('lists available actions on unknown action error', function () { $handler = new LiveComponentHandler(...); $component = new CounterComponent(...); try { $handler->handle($component, 'unknownAction', ActionParameters::empty()); expect(false)->toBeTrue(); } catch (FrameworkException $e) { expect($e->getData()['available_actions'])->toContain('increment'); expect($e->getData()['available_actions'])->toContain('decrement'); } }); ``` ### Integration Tests ```php it('recovers from state deserialization error', function () { $response = $this->post('/live-component/counter:test', [ 'action' => 'increment', 'state' => 'invalid json' // Invalid state ]); expect($response->status)->toBe(Status::BAD_REQUEST); expect($response->json('error'))->toBe('Invalid state format'); expect($response->json('recovery'))->toContain('reload'); }); it('returns state on rendering error', function () { // Component that fails rendering but action succeeded $response = $this->post('/live-component/broken:test', [ 'action' => 'increment', 'state' => ['count' => 5] ]); expect($response->json('success'))->toBeFalse(); expect($response->json('state'))->toBeArray(); expect($response->json('recovery'))->toBe('Try again'); }); ``` ## Monitoring ### Error Metrics - `livecomponent.error.rate` - Errors per second by type - `livecomponent.error.component` - Errors by component name - `livecomponent.error.action` - Errors by action name - `livecomponent.error.recovery_success_rate` - Recovery success rate ### Alerts - Error rate > 5% - Warning - Error rate > 10% - Critical - Specific component error rate > 20% - Component issue - Network error rate > 10% - Infrastructure issue ## Summary ✅ **Implemented:** - Component not found suggestions - Available actions listing - Safe state deserialization - Lifecycle hook error handling - Client-side retry logic ⚠️ **To Implement:** - Error code enum (FrameworkException integration) - Client-side error recovery UI - Comprehensive error testing - Error monitoring dashboard 📋 **Phase 1 Requirements:** - Implement all error codes - Add client-side error boundaries - Comprehensive error tests - Error monitoring integration