Skip to main content

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"
}
]
}
FieldTypeDescription
statusCodenumberHTTP status code
messagestring or arrayError description
errorstringError type
detailsarrayField-level validation errors (optional)

HTTP Status Codes

Success Codes

StatusMeaningUsage
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST
204No ContentSuccessful DELETE

Client Error Codes

StatusMeaningCommon Cause
400Bad RequestInvalid request body or parameters
401UnauthorizedMissing or invalid authentication
403ForbiddenValid auth but insufficient permissions
404Not FoundResource doesn't exist
409ConflictResource already exists
422Unprocessable EntityValidation error
429Too Many RequestsRate limit exceeded

Server Error Codes

StatusMeaningAction
500Internal Server ErrorRetry or report issue
502Bad GatewayCheck service status
503Service UnavailableWait and retry
504Gateway TimeoutRetry 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

  1. Handle all error types: Don't let unexpected errors crash your app
  2. Show user-friendly messages: Don't expose technical details to users
  3. Log errors for debugging: Include request ID and context
  4. Implement retry logic: Use exponential backoff for transient errors
  5. Refresh tokens proactively: Refresh before expiration when possible
  6. Monitor error rates: Track errors to detect issues early

Next Steps