Files
llink/js/desktop/src/lib/errors.ts
T
Arjun PatelandGitHub a8a0b7db1b infra: add linting and formatting for js projects (#230)
* wip

* wip

* wip

* format

* wip(mobile): lint and format

* cleanup

* nits

* nits

* idiomatic react

* nit

* format all root files
2026-06-02 07:44:24 -07:00

96 lines
2.9 KiB
TypeScript

import { z } from 'zod';
import { appEnv } from '@/config/env';
export class ApiError extends Error {
constructor(
public status: number,
message: string,
) {
super(message);
this.name = 'ApiError';
}
}
/**
* Thrown when a free-plan network attempts to create a non-container particle
* after hitting its daily message limit. Compose UI also disables triggers
* proactively via `useNetworkUsage` — this throw is a last-line defense.
*/
export class QuotaExceededError extends Error {
constructor(public readonly networkId: string) {
super('Daily message limit reached');
this.name = 'QuotaExceededError';
}
}
const IPC_PREFIX = /^Error invoking remote method '[^']+':\s*/;
function normalizeMessage(message: string): string {
return message
.replace(IPC_PREFIX, '')
.replace(/^Error:\s*/, '')
.trim();
}
export function toUserMessage(err: unknown): string {
if (err instanceof ApiError) {
if (err.status === 401) return 'Please sign in again.';
if (err.status === 403) return "You don't have permission to do that.";
if (err.status === 404) return 'Not found.';
if (err.status === 408 || err.status === 429) {
return 'Please try again in a moment.';
}
if (err.status >= 500) {
return 'Something went wrong on our end. Please try again.';
}
return normalizeMessage(err.message) || 'Request failed.';
}
if (err instanceof z.ZodError) {
return 'Received unexpected data from the server.';
}
if (err instanceof TypeError && /fetch|network/i.test(err.message)) {
return 'Network error. Check your connection.';
}
if (err instanceof Error) {
return normalizeMessage(err.message) || 'Something went wrong.';
}
return 'Something went wrong.';
}
type ErrorContext = Record<string, unknown>;
type ErrorSink = (err: unknown, context?: ErrorContext) => void;
// Sentry (or any observability backend) installs itself via `installErrorSinks`
// from renderer.tsx / main.ts. Until then, logError is a dev-only console call
// and reportError always prints — no call site needs to know.
let captureSink: ErrorSink | null = null;
let breadcrumbSink: ErrorSink | null = null;
export function installErrorSinks(sinks: {
capture: ErrorSink;
breadcrumb: ErrorSink;
}): void {
captureSink = sinks.capture;
breadcrumbSink = sinks.breadcrumb;
}
/** Expected-but-recordable failures. Breadcrumb only — never pages anyone. */
export function logError(err: unknown, context?: ErrorContext): void {
if (appEnv === 'dev') {
// eslint-disable-next-line no-console
console.error('[error]', err, context ?? {});
}
breadcrumbSink?.(err, context);
}
/** Unexpected failures the user may not see. Always captured. */
export function reportError(err: unknown, context?: ErrorContext): void {
// eslint-disable-next-line no-console
console.error('[error]', err, context ?? {});
captureSink?.(err, context);
}