Documentation Index
Fetch the complete documentation index at: https://grantmaster.dev/llms.txt
Use this file to discover all available pages before exploring further.
Error Handling & Logging
| Status | Updated | Covered Files |
|---|
| 🟢 Stable | 2026-04-05 | src/core/errorHandler.ts, src/core/BaseService.ts, packages/shared/src/errors/ |
Standard Error Pattern: ServiceResult
GrantMaster avoids throwing raw exceptions in business logic. Instead, we use the IServiceResult<T> pattern to return a predictable response.
Structure
interface IServiceResult<T> {
success: boolean;
data?: T;
error?: {
code: string; // e.g., 'PERMISSION_DENIED'
message: string; // Human readable for devs
details?: any; // Raw error or context
severity: 'low' | 'medium' | 'high' | 'critical';
};
}
Usage in Services
Use the withServiceResult wrapper in BaseService to automatically catch errors and log them.
async myAction() {
return this.withServiceResult(async () => {
// If this throws, it is caught and wrapped
const data = await someAsyncWork();
return data;
}, { operation: 'myAction' });
}
Error Classifications
| Code | Type | Action |
|---|
VALIDATION_ERROR | User | Show field errors in UI. |
PERMISSION_DENIED | Security | Redirect or show “Access Denied” toast. |
NOT_FOUND | Data | Show 404 or empty state. |
INTERNAL_ERROR | System | Alert Sentry and show “Something went wrong”. |
TENANT_MISMATCH | Critical | Immediate session termination + High-priority log. |
Logging & Monitoring
1. Application Logs (Structured Logger)
We use a structured logger (src/lib/logger.ts, re-exported via src/utils/logger.ts) which:
- Emits structured logs to Cloud Logging (server) or console (client)
- Adds Sentry breadcrumbs for info/warn
- Captures exceptions in
logger.error()
Log levels:
- Debug: Only in development.
- Info: Major business events (Project started, user joined).
- Warn: Recoverable issues (also breadcrumbs; optionally
captureMessage when explicitly requested).
- Error/Fatal: Caught exceptions.
2. Audit Logging (Firestore)
Critical business changes are persisted to the systemEvents collection via BaseService.logSuccess.
- Retention: Permanent (compliance requirement).
- Fields:
userId, organizationId, action, payload, timestamp.
3. Sentry Integration
Frontend Sentry is initialized in src/index.tsx and catches:
- React render crashes (via error boundaries)
- Manual reports via
logger.error() and handleError()
- Performance warnings via
Sentry.captureMessage() where explicitly used
Cloud Functions initialize Sentry via functions/src/core/sentry.ts (imported by functions/src/index.ts).
Error Code Catalog
ErrorSeverity Enum
Defined in packages/shared/src/errors/AppError.ts. Determines routing and alerting thresholds.
| Value | Meaning | Sentry Level | Toast Type |
|---|
INFO | Informational, no action needed | info | info |
WARNING | May need attention, not critical | warning | warning |
ERROR | Prevents operation, recoverable | error | error |
CRITICAL | Requires immediate attention | fatal | error |
ErrorCategory Enum
Defined in packages/shared/src/errors/AppError.ts. Used for grouping, filtering, and Sentry fingerprinting.
| Category | Value | Description |
|---|
VALIDATION | validation | Input/form validation failures |
AUTHENTICATION | authentication | Login failures, expired/invalid tokens |
AUTHORIZATION | authorization | Permission denied, insufficient role |
NOT_FOUND | not_found | Requested resource does not exist |
NETWORK | network | Fetch/connection failures |
FIRESTORE | firestore | Firestore read/write/query failures |
STORAGE | storage | Firebase Storage upload/download failures |
RATE_LIMIT | rate_limit | API throttling, quota exceeded |
BUSINESS_LOGIC | business_logic | Domain-specific validation (e.g., budget exceeded) |
EXTERNAL_API | external_api | Third-party API failures (Stripe, Postmark, HubSpot) |
AI_SERVICE | ai_service | Gemini/Genkit/embedding failures |
EMAIL_SERVICE | email_service | Postmark send/template failures |
UNKNOWN | unknown | Catch-all for unrecognized errors |
Error Class Hierarchy
All classes live in packages/shared/src/errors/ and are re-exported from src/errors/. The hierarchy has two branches below AppError: UserError (displayed to users, severity WARNING) and SystemError (generic “something went wrong” message, severity ERROR).
AppError (abstract)
├── UserError
│ ├── ValidationError — Input validation failures
│ ├── AuthorizationError — Permission denied (userMessage: "You do not have permission…")
│ ├── NotFoundError — Resource not found (accepts resource name + id)
│ ├── RateLimitError — API throttling (accepts optional retryAfter seconds)
│ └── BusinessLogicError — Domain rule violations (userMessage = message)
│
└── SystemError
├── AuthenticationError — Login/token failures
├── FirestoreError — Firestore operation failures (accepts operation + original error)
├── StorageError — Firebase Storage failures (accepts operation + original error)
├── NetworkError — Fetch/connectivity failures
├── ExternalAPIError — Third-party API failures (accepts service name + original error)
├── AIServiceError — Gemini/Genkit failures (accepts operation + original error)
└── EmailServiceError — Postmark failures (accepts operation + original error)
Domain-Specific Error Classes
Defined in packages/shared/src/errors/domain.ts. These extend Error directly (not AppError) and carry domain-specific metadata with toJSON() serialization.
| Class | Fields | Use Case |
|---|
RateLimitExceededError | limit, used, window, retryAfter | Quota enforcement with detailed usage info |
TenantIsolationError | attemptedOrganizationId, userOrganizationId, operation, timestamp | Critical security error — cross-tenant access attempt |
FeatureNotAvailableError | feature, currentTier, requiredTier, upgradeUrl | Subscription tier gating |
Standalone Error Class
| Class | Location | Use Case |
|---|
RuntimeEnvValidationError | src/lib/runtimeEnv.ts | Thrown at boot when required VITE_* env vars are missing or invalid. Carries an issues: string[] array. |
Sentry Integration Details
Frontend (src/lib/sentryBrowser.ts)
Sentry is loaded lazily via dynamic import('@sentry/react') and only in production when VITE_SENTRY_DSN is set. Initialization happens in src/bootstrap.tsx.
Configuration:
| Setting | Value |
|---|
tracesSampleRate | 0.1 (10%) |
profilesSampleRate | 0.1 (10%) |
replaysSessionSampleRate | 0.1 (10%) |
replaysOnErrorSampleRate | 1.0 (100% on errors) |
sendDefaultPii | false |
environment | import.meta.env.MODE |
release | __SENTRY_RELEASE__ (injected at build time) |
Data scrubbing: The beforeSend hook replaces Authorization headers with [Scrubbed].
Integrations: browserTracingIntegration, replayIntegration.
Exported helpers (all async, no-op when Sentry is not loaded):
| Function | Purpose |
|---|
captureSentryException(error, context?) | Report an error to Sentry |
captureSentryMessage(message, context?) | Report a message |
addSentryBreadcrumb(breadcrumb) | Add navigation/action breadcrumb |
setSentryBrowserUser(user) | Set user context (id, email) |
setSentryBrowserTag(key, value) | Set global tag |
setSentryBrowserContext(key, context) | Set structured context |
showSentryReportDialog(eventId?) | Open user feedback dialog |
startSentryInactiveSpan(options) | Start a performance span |
setSentryMeasurement(name, value, unit) | Record a custom measurement |
recordSentryDistribution(name, value, options) | Record a metrics distribution |
Cloud Functions (functions/src/core/sentry.ts)
Uses @sentry/node, also lazy-loaded to avoid blocking Firebase CLI’s function-discovery process. Disabled when FUNCTIONS_EMULATOR=true.
Key differences from frontend:
- Uses
getIsolationScope() for per-request tenant/user tagging (organizationId, authType, requestId, userId).
withSentryIsolationScope(scope, fn) wraps an async operation with scoped tags.
captureException() calls Sentry.flush() (default 2 s timeout) to ensure delivery before the function terminates.
- Release is set from
SENTRY_RELEASE or GITHUB_SHA (uploaded via CI).
What Gets Sent to Sentry (Filtering Rules)
The ErrorHandler.shouldCaptureInSentry() method in src/core/errorHandler.ts applies these filters:
- Never in development (
import.meta.env.DEV).
- Never for INFO or WARNING severity.
- Never for user-error categories:
VALIDATION, NOT_FOUND, AUTHORIZATION.
- Everything else (system errors, Firestore errors, network errors, external API errors, AI service errors) is captured.
Sentry tags applied per error:
category — the ErrorCategory value
severity — the ErrorSeverity value
error_class — the constructor name (e.g., FirestoreError)
Fingerprinting: [error.category, error.message] — groups similar errors together.
Error Boundaries in React
Three error boundary components provide layered crash isolation:
1. SentryErrorBoundary (src/components/SentryErrorBoundary.tsx)
- Scope: Wraps the entire
<App /> in src/bootstrap.tsx — the outermost catch.
- Behavior: Captures to Sentry, shows full-page “Something went wrong” card with “Reload Application” button and “Report feedback” link (opens
Sentry.showReportDialog).
- Displays: Sentry event ID in footer for support reference.
2. RouteErrorBoundary (src/components/ui/RouteErrorBoundary.tsx)
- Scope: Wraps individual routes and page sections.
- Isolation levels:
'route' (full-page fallback, default) or 'section' (compact inline card).
- Behavior: Captures to Sentry with
isolationLevel context. Supports custom fallbackComponent prop. “Try Again” resets state; “Go to Overview” navigates to /overview.
- Dev mode: Shows error message in a code block.
3. ErrorBoundary (src/components/ui/ErrorBoundary.tsx)
- Scope: Wraps individual components or widget areas.
- Features: Accepts
context string (sent as Sentry tag errorBoundary), onError callback, onReset handler, optional fallback ReactNode, showDetails toggle.
- Dev mode: Shows full error details and component stack trace.
- Recovery: “Try Again” button with repeated-failure warning.
- Scope: Wraps individual dashboard widgets.
- Purpose: Prevents a single broken widget from crashing the entire dashboard.
Error Handling Patterns by Layer
Service Layer
Services use BaseService.withServiceResult() to wrap async operations. Errors are caught, logged via the service’s structured logger, and returned as { success: false, error: string }. Services do not show toasts or capture to Sentry directly.
// In a service extending BaseService
async archiveProject(id: string): Promise<ServiceResult> {
return this.withServiceResult(
async () => {
await this.update(id, { archived: true });
},
{ operation: 'archiveProject', entityId: id }
);
}
For throwing typed errors within services:
import { NotFoundError, BusinessLogicError } from '@grantmaster/shared/errors';
if (!project) throw new NotFoundError('Project', projectId);
if (project.budget <= 0) throw new BusinessLogicError('Budget must be positive');
Hook Layer
Two hooks provide error handling for React components:
useErrorHandler (src/hooks/useErrorHandler.ts):
handleError(error, context?, options?) — logs, toasts, and captures to Sentry.
handleErrorSilent(error, context?) — logs and captures but no toast.
withErrorBoundary(fn, context?) — wraps an async function with automatic error handling.
useAsyncAction (src/hooks/useAsyncAction.ts):
- Wraps an async action with loading state, error state, duplicate-execution prevention, success/error toasts, and automatic
handleError integration.
const { execute, isLoading } = useAsyncAction({
action: () => projectService.archiveProject(id),
successMessage: 'Project archived',
errorContext: { operation: 'archiveProject', projectId: id },
});
Component Layer
Components should:
- Wrap risky subtrees with
<ErrorBoundary context="...">.
- Use
useErrorHandler() for imperative error handling in event handlers.
- Use
useAsyncAction() for button-triggered async operations.
- Check
ServiceResult.success before accessing .data.
function ProjectActions({ projectId }: Props) {
const { handleError } = useErrorHandler();
const handleArchive = async () => {
const result = await projectService.archiveProject(projectId);
if (!result.success) {
// Error already logged by service — optionally show custom UI
return;
}
toast.success('Project archived');
};
return (
<ErrorBoundary context="Project Actions">
<Button onClick={handleArchive}>Archive</Button>
</ErrorBoundary>
);
}
tRPC / Cloud Functions Layer
Cloud Functions use functions/src/core/sentry.ts:
- Wrap request handlers with
withSentryIsolationScope(scope, fn) for per-request user/org tagging.
- Call
captureException(error, { scope, extra, tags }) for unrecoverable errors.
- Sentry flushes before function termination to avoid dropped events.
Maintenance
Update this document when:
- Adding a new global error code.
- Adding or removing an error class from the hierarchy.
- Changing the severity levels.
- Modifying Sentry filtering rules.
- Adding a new error boundary.
- Switching logging providers.