Skip to main content

Error Handling

This library maps Mastodon API errors to a sealed class hierarchy rooted at MastodonException.

Exception hierarchy​

MastodonException (sealed)
├── MastodonApiException // General HTTP response errors
│ ├── MastodonUnauthorizedException // 401 - Authentication error
│ ├── MastodonForbiddenException // 403 - Permission error
│ ├── MastodonNotFoundException // 404 - Resource not found
│ ├── MastodonRateLimitException // 429 - Rate limited
│ │ ├── retryAfter // Recommended wait duration
│ │ ├── limit // Request limit for the window
│ │ ├── remaining // Remaining requests
│ │ └── resetAt // Time when the window resets
│ ├── MastodonValidationException // 422 - Validation error
│ │ ├── serverMessage // Detailed server message
│ │ ├── details // Field errors by request field
│ │ └── MastodonAlreadyVotedException // Already voted
│ └── MastodonServerException // 5xx - Server error
├── MastodonNetworkException // Network connection error
└── MastodonAuthException (sealed) // OAuth authentication flow errors
├── MastodonAuthCancelledException // User cancelled
├── MastodonAuthStateMismatchException // CSRF detected
└── MastodonAuthTokenException // Token acquisition failed

Basic catch patterns​

Catching all errors​

try {
final status = await client.statuses.fetch(id);
} on MastodonException catch (e) {
print('Error: $e');
}

Handling by HTTP status​

try {
final account = await client.accounts.fetchById(accountId);
} on MastodonNotFoundException {
print('Account not found');
} on MastodonUnauthorizedException {
print('Token is invalid. Please re-authenticate');
} on MastodonRateLimitException catch (e) {
print('Rate limited. Retry after ${e.retryAfter}');
} on MastodonApiException catch (e) {
print('API error (${e.statusCode}): ${e.message}');
} on MastodonNetworkException {
print('Check your network connection');
}

Getting validation error details​

try {
await client.accounts.create(request);
} on MastodonValidationException catch (e) {
print('Validation error: ${e.serverMessage}');
final details = e.details;
if (details != null) {
for (final entry in details.entries) {
for (final detail in entry.value) {
print('${entry.key}: ${detail.code} - ${detail.description ?? ''}');
}
}
}
}

details is keyed by request field. Each MastodonValidationErrorDetail contains the server's error code and optional description. Unknown codes are preserved as strings for forward compatibility. details is null when an endpoint does not return Mastodon's recognized validation-details shape.

Special exceptions​

MastodonAlreadyVotedException​

Thrown when attempting to vote on a poll that has already been voted on.

try {
await client.polls.vote(pollId, [0]);
} on MastodonAlreadyVotedException {
print('Already voted on this poll');
}

Handling rate limits​

MastodonRateLimitException exposes Mastodon's X-RateLimit-* headers. The recommended wait uses Retry-After when a server or proxy provides it, then falls back to the X-RateLimit-Reset timestamp.

Future<T> withRetry<T>(Future<T> Function() action) async {
try {
return await action();
} on MastodonRateLimitException catch (e) {
print('${e.remaining}/${e.limit} requests remain; reset at ${e.resetAt}');
final wait = e.retryAfter ?? const Duration(seconds: 60);
await Future<void>.delayed(wait);
return action();
}
}

// Usage
final timeline = await withRetry(
() => client.timelines.fetchHome(limit: 20),
);