Error Handling
Handle AsaHome Cloud API errors gracefully in your applications.
Error Response Format
All API errors follow a consistent format:
{
"statusCode": 400,
"message": "Validation failed",
"error": "Bad Request",
"details": [
{
"field": "email",
"message": "email must be a valid email address"
}
]
}
| Field | Type | Description |
|---|---|---|
statusCode | number | HTTP status code |
message | string or array | Error description |
error | string | Error type |
details | array | Field-level validation errors (optional) |
HTTP Status Codes
Success Codes
| Status | Meaning | Usage |
|---|---|---|
200 | OK | Successful GET, PUT, PATCH |
201 | Created | Successful POST |
204 | No Content | Successful DELETE |
Client Error Codes
| Status | Meaning | Common Cause |
|---|---|---|
400 | Bad Request | Invalid request body or parameters |
401 | Unauthorized | Missing or invalid authentication |
403 | Forbidden | Valid auth but insufficient permissions |
404 | Not Found | Resource doesn't exist |
409 | Conflict | Resource already exists |
422 | Unprocessable Entity | Validation error |
429 | Too Many Requests | Rate limit exceeded |
Server Error Codes
| Status | Meaning | Action |
|---|---|---|
500 | Internal Server Error | Retry or report issue |
502 | Bad Gateway | Check service status |
503 | Service Unavailable | Wait and retry |
504 | Gateway Timeout | Retry with longer timeout |
Common Errors
Authentication Errors
Invalid Credentials (401)
{
"statusCode": 401,
"message": "Invalid email or password",
"error": "Unauthorized"
}
Token Expired (401)
{
"statusCode": 401,
"message": "Token has expired",
"error": "Unauthorized"
}
Solution: Refresh the access token using your refresh token.
Invalid Token (401)
{
"statusCode": 401,
"message": "Invalid token",
"error": "Unauthorized"
}
Solution: Re-authenticate to get new tokens.
Authorization Errors
Insufficient Permissions (403)
{
"statusCode": 403,
"message": "You do not have permission to access this resource",
"error": "Forbidden"
}
Device Access Denied (403)
{
"statusCode": 403,
"message": "Only the device owner can update device settings",
"error": "Forbidden"
}
Validation Errors
Single Field (400)
{
"statusCode": 400,
"message": "email must be a valid email address",
"error": "Bad Request"
}
Multiple Fields (400)
{
"statusCode": 400,
"message": [
"email must be a valid email address",
"password must be at least 8 characters"
],
"error": "Bad Request"
}
Resource Errors
Not Found (404)
{
"statusCode": 404,
"message": "Device not found",
"error": "Not Found"
}
Conflict (409)
{
"statusCode": 409,
"message": "Device with this UUID already exists",
"error": "Conflict"
}
Rate Limiting (429)
{
"statusCode": 429,
"message": "Too many requests",
"error": "Too Many Requests",
"retryAfter": 60
}
Flutter Error Handling
Error Classes
abstract class ApiException implements Exception {
final String message;
final int statusCode;
ApiException(this.message, this.statusCode);
}
class UnauthorizedException extends ApiException {
UnauthorizedException([String message = 'Unauthorized'])
: super(message, 401);
}
class ForbiddenException extends ApiException {
ForbiddenException([String message = 'Forbidden'])
: super(message, 403);
}
class NotFoundException extends ApiException {
NotFoundException([String message = 'Not found'])
: super(message, 404);
}
class ValidationException extends ApiException {
final List<String> errors;
ValidationException(this.errors)
: super(errors.join(', '), 400);
}
class RateLimitException extends ApiException {
final int retryAfter;
RateLimitException(this.retryAfter)
: super('Rate limit exceeded', 429);
}
class ServerException extends ApiException {
ServerException([String message = 'Server error'])
: super(message, 500);
}
HTTP Client with Error Handling
import 'dart:convert';
import 'package:http/http.dart' as http;
class ApiClient {
final String baseUrl;
final AuthStorage authStorage;
ApiClient({
required this.baseUrl,
required this.authStorage,
});
Future<Map<String, dynamic>> get(String path) async {
final response = await _request('GET', path);
return jsonDecode(response.body);
}
Future<Map<String, dynamic>> post(String path, Map<String, dynamic> body) async {
final response = await _request('POST', path, body: body);
return jsonDecode(response.body);
}
Future<http.Response> _request(
String method,
String path, {
Map<String, dynamic>? body,
}) async {
final token = await authStorage.getAccessToken();
final uri = Uri.parse('$baseUrl$path');
final headers = {
'Content-Type': 'application/json',
if (token != null) 'Authorization': 'Bearer $token',
};
http.Response response;
switch (method) {
case 'GET':
response = await http.get(uri, headers: headers);
break;
case 'POST':
response = await http.post(uri, headers: headers, body: jsonEncode(body));
break;
case 'PUT':
response = await http.put(uri, headers: headers, body: jsonEncode(body));
break;
case 'DELETE':
response = await http.delete(uri, headers: headers);
break;
default:
throw UnsupportedError('Method $method not supported');
}
_handleErrors(response);
return response;
}
void _handleErrors(http.Response response) {
if (response.statusCode >= 200 && response.statusCode < 300) {
return;
}
final body = jsonDecode(response.body);
final message = body['message'];
switch (response.statusCode) {
case 400:
if (message is List) {
throw ValidationException(List<String>.from(message));
}
throw ValidationException([message.toString()]);
case 401:
throw UnauthorizedException(message?.toString() ?? 'Unauthorized');
case 403:
throw ForbiddenException(message?.toString() ?? 'Forbidden');
case 404:
throw NotFoundException(message?.toString() ?? 'Not found');
case 429:
throw RateLimitException(body['retryAfter'] ?? 60);
default:
throw ServerException(message?.toString() ?? 'Server error');
}
}
}
Using the Error Handling
class DeviceRepository {
final ApiClient client;
DeviceRepository(this.client);
Future<List<Device>> getDevices() async {
try {
final data = await client.get('/devices');
return (data['data'] as List)
.map((json) => Device.fromJson(json))
.toList();
} on UnauthorizedException {
// Token expired - trigger refresh
throw AuthenticationRequired();
} on RateLimitException catch (e) {
// Wait and retry
await Future.delayed(Duration(seconds: e.retryAfter));
return getDevices();
} on NotFoundException {
// Return empty list
return [];
} on ValidationException catch (e) {
// Show validation errors to user
throw UserFacingError(e.errors.join('\n'));
} on ServerException {
// Log and show generic error
throw UserFacingError('Something went wrong. Please try again.');
}
}
}
Showing Errors to Users
void showError(BuildContext context, ApiException error) {
String message;
if (error is ValidationException) {
message = error.errors.join('\n');
} else if (error is RateLimitException) {
message = 'Too many requests. Please wait ${error.retryAfter} seconds.';
} else if (error is UnauthorizedException) {
message = 'Your session has expired. Please log in again.';
// Navigate to login
Navigator.of(context).pushReplacementNamed('/login');
return;
} else {
message = 'An error occurred. Please try again.';
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
}
Retry Strategies
Exponential Backoff
Future<T> withRetry<T>(
Future<T> Function() fn, {
int maxRetries = 3,
Duration initialDelay = const Duration(seconds: 1),
}) async {
int attempts = 0;
Duration delay = initialDelay;
while (true) {
try {
return await fn();
} on ServerException {
attempts++;
if (attempts >= maxRetries) rethrow;
await Future.delayed(delay);
delay *= 2; // Exponential backoff
} on RateLimitException catch (e) {
await Future.delayed(Duration(seconds: e.retryAfter));
}
}
}
// Usage
final devices = await withRetry(() => deviceRepository.getDevices());
Retry with Jitter
import 'dart:math';
Duration addJitter(Duration delay) {
final jitter = Random().nextDouble() * 0.3; // 0-30% jitter
return delay * (1 + jitter);
}
Best Practices
- Handle all error types: Don't let unexpected errors crash your app
- Show user-friendly messages: Don't expose technical details to users
- Log errors for debugging: Include request ID and context
- Implement retry logic: Use exponential backoff for transient errors
- Refresh tokens proactively: Refresh before expiration when possible
- Monitor error rates: Track errors to detect issues early
Next Steps
- Rate Limiting - Understand rate limits
- Authentication - Token management
- Troubleshooting - Debug common issues