Skip to main content

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

StatusCategoryDescription
400Bad RequestInvalid input or nested threads
401UnauthorizedAuthentication failure
404Not FoundResource doesn’t exist
409ConflictResource already exists
429Rate LimitedToo many requests
500Internal ErrorUnexpected server error
503Service UnavailableInfrastructure 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

Build docs developers (and LLMs) love