Overview
Hazel Chat uses typed errors based on Effect Schema, providing compile-time type safety and runtime validation. All errors include an HTTP status code and detailed information for debugging.
Error Structure
All API errors follow this structure:
interface ApiError {
_tag : string // Unique error type identifier
message : string // Human-readable error message
detail ?: string // Additional context (optional)
[ key : string ] : any // Error-specific fields
}
Errors are tagged unions, enabling exhaustive pattern matching:
Effect . catchTags ({
UnauthorizedError : ( error ) => handleUnauthorized ( error ),
MessageNotFoundError : ( error ) => handleNotFound ( error ),
InternalServerError : ( error ) => handleServerError ( error )
})
HTTP Status Codes
Status Category Description 400 Bad Request Invalid input or nested threads 401 Unauthorized Authentication failure 404 Not Found Resource doesn’t exist 409 Conflict Resource already exists 429 Rate Limited Too many requests 500 Internal Error Unexpected server error 503 Service Unavailable Infrastructure or workflow service down
Error Types
Authentication Errors (401)
UnauthorizedError
Generic authorization failure. User lacks permission for the requested operation.
{
_tag : "UnauthorizedError" ,
message : "Permission denied" ,
detail : "You don't have access to this resource"
}
Common causes:
User not a member of the channel/organization
Insufficient role permissions
Attempting to modify another user’s content
How to handle:
Effect . catchTag ( "UnauthorizedError" , ( error ) =>
Effect . gen ( function* () {
// Show permission denied message
yield * showErrorToast ( "You don't have permission for this action" )
// Optionally redirect
yield * Effect . sync (() => router . push ( "/channels" ))
})
)
SessionNotProvidedError
No authentication credentials provided.
{
_tag : "SessionNotProvidedError" ,
message : "No session cookie or Authorization header" ,
detail : "Authentication required"
}
How to handle:
Effect . catchTag ( "SessionNotProvidedError" , () =>
// Redirect to login
Effect . sync (() => window . location . href = "/auth/login" )
)
SessionExpiredError
Session token has expired.
{
_tag : "SessionExpiredError" ,
message : "Session has expired" ,
detail : "Please re-authenticate"
}
How to handle:
Effect . catchTag ( "SessionExpiredError" , () =>
Effect . gen ( function* () {
// Attempt session refresh
const refreshed = yield * refreshSession ()
if ( refreshed ) {
// Retry original request
return yield * originalRequest
} else {
// Redirect to login
yield * Effect . sync (() => window . location . href = "/auth/login" )
}
})
)
InvalidBearerTokenError
JWT verification failed (invalid signature or format).
{
_tag : "InvalidBearerTokenError" ,
message : "JWT verification failed" ,
detail : "Invalid signature or expired token"
}
Common causes:
Bot token is incorrect
Token signature doesn’t match
Token format is malformed
InvalidJwtPayloadError
JWT payload is missing required fields.
{
_tag : "InvalidJwtPayloadError" ,
message : "Token missing user ID" ,
detail : "The provided token is missing the user ID"
}
SessionAuthenticationError
Generic session authentication failure.
{
_tag : "SessionAuthenticationError" ,
message : "Authentication failed" ,
detail : "Unable to verify session"
}
Resource Not Found (404)
MessageNotFoundError
Requested message doesn’t exist.
{
_tag : "MessageNotFoundError" ,
messageId : "msg_123" // The ID that wasn't found
}
Used in:
message.update
message.delete
channel.createThread (if parent message not found)
How to handle:
Effect . catchTag ( "MessageNotFoundError" , ( error ) =>
Effect . gen ( function* () {
yield * showErrorToast ( `Message ${ error . messageId } not found` )
// Optionally refresh message list
yield * refreshMessages ()
})
)
ChannelNotFoundError
Requested channel doesn’t exist.
{
_tag : "ChannelNotFoundError" ,
channelId : "ch_456" // The ID that wasn't found
}
Used in:
channel.update
channel.delete
channel.generateName
ThreadChannelNotFoundError
Thread channel not found in workflow.
{
_tag : "ThreadChannelNotFoundError" ,
message : string ,
channelId : string
}
Used in: Thread naming workflow
OriginalMessageNotFoundError
Original message for thread not found.
{
_tag : "OriginalMessageNotFoundError" ,
message : string ,
threadId : string
}
Used in: Thread naming workflow
Bad Request (400)
NestedThreadError
Attempted to create a thread within another thread.
{
_tag : "NestedThreadError" ,
channelId : "ch_thread_123" // The thread channel ID
}
Why it happens: Hazel doesn’t support nested threads (threads within threads).
How to handle:
Effect . catchTag ( "NestedThreadError" , () =>
showErrorToast ( "Cannot create threads within threads" )
)
Conflict (409)
DmChannelAlreadyExistsError
Direct message channel already exists between users.
{
_tag : "DmChannelAlreadyExistsError" ,
message : "DM channel already exists" ,
detail ?: string // Optional additional info
}
How to handle:
Effect . catchTag ( "DmChannelAlreadyExistsError" , () =>
Effect . gen ( function* () {
// Find existing DM and navigate to it
const existingDm = yield * findExistingDm ( userId )
yield * navigateToChannel ( existingDm . id )
})
)
OAuthCodeExpiredError
OAuth authorization code has expired or was already used.
{
_tag : "OAuthCodeExpiredError" ,
message : "Authorization code expired"
}
How to handle: Restart OAuth flow
Rate Limiting (429)
RateLimitExceededError
User exceeded rate limit (60 requests/minute).
{
_tag : "RateLimitExceededError" ,
message : "Rate limit exceeded" ,
retryAfterMs : 5000 , // Wait before retry (milliseconds)
limit : 60 , // Total limit
remaining : 0 // Requests remaining
}
How to handle:
Effect . catchTag ( "RateLimitExceededError" , ( error ) =>
Effect . gen ( function* () {
// Show user-friendly message
yield * showErrorToast (
`Too many requests. Please wait ${ error . retryAfterMs / 1000 } s`
)
// Wait and retry
yield * Effect . sleep ( error . retryAfterMs )
return yield * Effect . retry ( originalRequest , {
schedule: "exponential" ,
times: 3
})
})
)
Implement exponential backoff to avoid repeatedly hitting rate limits.
Internal Server Errors (500)
InternalServerError
Unexpected server error.
{
_tag : "InternalServerError" ,
message : string , // High-level error description
detail ?: string , // Additional context
cause ?: any // Underlying cause (if available)
}
How to handle:
Effect . catchTag ( "InternalServerError" , ( error ) =>
Effect . gen ( function* () {
// Log for debugging
console . error ( "Server error:" , error )
// Show user-friendly message
yield * showErrorToast ( "Something went wrong. Please try again." )
// Report to error tracking (Sentry, etc.)
yield * reportError ( error )
})
)
WorkflowInitializationError
Workflow service initialization failed.
{
_tag : "WorkflowInitializationError" ,
message : string ,
cause ?: any
}
Used in: Thread naming and other workflow operations
Service Unavailable (503)
SessionLoadError
Failed to load session from database or cache.
{
_tag : "SessionLoadError" ,
message : "Failed to load session" ,
detail : "Database connection error"
}
How to handle: Can be retried (503 is temporary)
Effect . catchTag ( "SessionLoadError" , () =>
Effect . retry ( originalRequest , {
schedule: "exponential" ,
times: 3 ,
delay: "1 second"
})
)
WorkflowServiceUnavailableError
Workflow service (cluster) is unreachable.
{
_tag : "WorkflowServiceUnavailableError" ,
message : "Workflow service unavailable" ,
cause ?: string | null
}
Used in: AI thread naming (channel.generateName)
How to handle:
Effect . catchTag ( "WorkflowServiceUnavailableError" , () =>
Effect . gen ( function* () {
yield * showErrorToast ( "AI service temporarily unavailable" )
// Fallback: Allow manual naming
yield * promptForManualName ()
})
)
WorkOSUserFetchError
Failed to fetch user data from WorkOS.
{
_tag : "WorkOSUserFetchError" ,
message : "Failed to fetch user from WorkOS" ,
detail : string
}
AI/Workflow Errors
These errors occur during AI-powered operations (like thread naming):
AIProviderUnavailableError
AI service (OpenAI, etc.) is unreachable.
{
_tag : "AIProviderUnavailableError" ,
message : string ,
provider ?: string // e.g., "openai"
}
AIRateLimitError
AI service rate limit exceeded.
{
_tag : "AIRateLimitError" ,
message : string ,
retryAfter ?: number
}
AIResponseParseError
Failed to parse AI response.
{
_tag : "AIResponseParseError" ,
message : string ,
response ?: string
}
ThreadContextQueryError
Database query for thread context failed.
{
_tag : "ThreadContextQueryError" ,
message : string ,
cause ?: any
}
ThreadNameUpdateError
Failed to update thread name in database.
{
_tag : "ThreadNameUpdateError" ,
message : string ,
cause ?: any
}
Error Handling Patterns
Exhaustive Tag Matching
const result = yield * client [ "message.create" ]( payload ). pipe (
Effect . catchTags ({
UnauthorizedError: handleUnauthorized ,
MessageNotFoundError: handleNotFound ,
RateLimitExceededError: handleRateLimit ,
InternalServerError: handleServerError
})
)
Retry with Exponential Backoff
const result = yield * client [ "message.create" ]( payload ). pipe (
Effect . retry ({
schedule: "exponential" ,
times: 3 ,
delay: "1 second"
}),
Effect . catchTag ( "RateLimitExceededError" , ( error ) =>
Effect . sleep ( error . retryAfterMs ). pipe (
Effect . andThen ( Effect . retry ( operation , { times: 1 }))
)
)
)
Fallback Strategies
const result = yield * client [ "channel.generateName" ]({
channelId: "thread-id"
}). pipe (
Effect . catchTags ({
WorkflowServiceUnavailableError : () =>
// Fallback: Use default thread name
Effect . succeed ({ success: false }),
AIProviderUnavailableError : () =>
// Fallback: Generate simple name
Effect . succeed ({ success: false })
})
)
if ( ! result . success ) {
// Use fallback thread name
yield * setThreadName ( channelId , "New Thread" )
}
Graceful Degradation
const uploadResult = yield * client [ "attachment.create" ]( file ). pipe (
Effect . catchTag ( "InternalServerError" , () =>
Effect . gen ( function* () {
// Degrade: Send message without attachment
yield * showWarning ( "Failed to upload attachment" )
return yield * client [ "message.create" ]({
... payload ,
attachmentIds: [] // Send without attachment
})
})
)
)
Error Reporting
Always report unexpected errors to your error tracking service (Sentry, Datadog, etc.) for monitoring and debugging.
import * as Sentry from "@sentry/browser"
const reportError = ( error : ApiError ) =>
Effect . sync (() => {
Sentry . captureException ( error , {
tags: {
errorType: error . _tag ,
errorMessage: error . message
},
extra: {
detail: error . detail ,
... error // Include all error fields
}
})
})
// Use in error handling
Effect . catchTag ( "InternalServerError" , ( error ) =>
Effect . gen ( function* () {
yield * reportError ( error )
yield * showErrorToast ( "Something went wrong" )
})
)
Best Practices
1. Handle Specific Errors First
Effect . catchTags ({
// Specific errors first
RateLimitExceededError: handleRateLimit ,
MessageNotFoundError: handleNotFound ,
// Generic fallback last
InternalServerError: handleGenericError
})
2. Provide User-Friendly Messages
const friendlyMessages = {
UnauthorizedError: "You don't have permission for this action" ,
MessageNotFoundError: "This message no longer exists" ,
RateLimitExceededError: "You're doing that too quickly" ,
InternalServerError: "Something went wrong. Please try again."
}
3. Log for Debugging
Effect . catchTag ( "InternalServerError" , ( error ) =>
Effect . gen ( function* () {
// Log full error details
console . error ( "API Error:" , {
tag: error . _tag ,
message: error . message ,
detail: error . detail ,
cause: error . cause
})
// Show simple message to user
yield * showErrorToast ( "Something went wrong" )
})
)
4. Implement Retry Logic
const withRetry = < A , E >( effect : Effect . Effect < A , E >) =>
effect . pipe (
Effect . retry ({
schedule: "exponential" ,
times: 3 ,
delay: "1 second"
}),
Effect . catchAll (( error ) =>
Effect . gen ( function* () {
yield * reportError ( error )
return yield * Effect . fail ( error )
})
)
)
Don’t retry 401 (Unauthorized) or 400 (Bad Request) errors. These won’t succeed without changing the request.
Next Steps
Authentication Learn about authentication errors in detail
API Introduction Return to API overview and architecture