Logging and errors
app.log writes the app’s log to the Log Center, where administrators see it next to the system’s logs, and to the browser console. Every failed SDK call rejects with a PithyxError that carries a stable code (area.object.reason), its params, an English detail and, when the daemon was involved, a requestId. Decide on the code, never on the text: retry what may pass by itself, and show the rest in the user’s language.
Example
Section titled “Example”// Logging to the Log Center, telling retryable failures from final ones, and showing errors in the user's language.import { isProblem, PithyxError, type PithyxApp } from '@pithyx/sdk';import { createI18n, type I18n } from '@pithyx/sdk/i18n';
/** Failures that may pass by themselves: the desktop or the box was busy, restarting or starting the backend. */const RETRYABLE = new Set([ 'sdk.request.timeout', 'sdk.backend.unreachable', 'app.backend.unavailable', 'auth.rate.limited',]);
function codeOf(error: unknown): string { return error instanceof PithyxError || isProblem(error) ? error.code : 'unknown';}
function wait(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms));}
/** Runs `work` up to `attempts` times, waiting 1, 2, 4 ... seconds (or what the box asks for) between tries. */export async function withRetry<T>( app: PithyxApp, work: () => Promise<T>, attempts = 4,): Promise<T> { for (let attempt = 1; ; attempt++) { try { return await work(); } catch (error) { const code = codeOf(error); if (!RETRYABLE.has(code) || attempt >= attempts) { throw error; } const after = error instanceof PithyxError ? Number(error.params.retryAfterSeconds) : NaN; const delay = Number.isFinite(after) ? after * 1000 : 1000 * 2 ** (attempt - 1); app.log.debug('Retrying', { code, attempt, delay }); await wait(delay); } }}
/** A backend answer as a value, or its problem as a PithyxError, so withRetry sees the 503 of a stopped backend. */export async function backendJson<T>(app: PithyxApp, path: string): Promise<T> { const response = await app.backend.fetch(path); if (response.ok) { return (await response.json()) as T; } const body: unknown = await response.json().catch(() => null); throw new PithyxError( isProblem(body) ? body : { code: 'app.backend.unavailable', params: {}, detail: `HTTP ${String(response.status)}` }, );}
// The app's own texts for the codes it expects, nested by the dots of the code; a sandboxed app has no access to// the desktop's texts.const i18n: (app: PithyxApp) => I18n = (app) => createI18n(app, { en: { errors: { app: { backend: { unavailable: 'The service is starting or not running. Try again in a moment.', }, permission: { denied: 'An administrator has not allowed this for the app.' }, storage: { too_large: 'Too much data to save (limit {maxKb} KB).' }, }, sdk: { backend: { unreachable: 'The box cannot be reached. Check the connection.' } }, unknown: 'Something went wrong.', }, }, });
/** Logs a failure with its code and request id, and shows it: the desktop's dialog where it exists, else own text. */export async function report( app: PithyxApp, error: unknown, show: (text: string) => void,): Promise<void> { const code = codeOf(error); const requestId = error instanceof PithyxError ? (error.requestId ?? null) : null; app.log.error('Action failed', { code, requestId }); if (app.supports('ui.showError')) { await app.ui.showError(error); return; } const t = i18n(app); const params = error instanceof PithyxError ? error.params : {}; // t() returns the key itself for a code without a text. const text = t.t(`errors.${code}`, params); show(text === `errors.${code}` ? t.t('errors.unknown') : text);}
export async function loadDashboard(app: PithyxApp, show: (text: string) => void) { try { const stats = await withRetry(app, () => backendJson<{ files: number }>(app, '/stats')); app.log.info('Dashboard loaded', { files: stats.files }); } catch (error) { await report(app, error, show); }}Logging
Section titled “Logging”app.log.debug,info,warn,errorandlog(the same asinfo) take a message and optionaldata, a flat object of strings, numbers, booleans andnull. They return at once and never throw.- Every entry appears in the browser console at its level.
info,warnanderroralso go to the daemon;debugonly while an administrator switched on debug logging for the app (context.logLevelisdebug,logLevelChangefollows it). - In the Log Center entries appear under the source
app/<id>with the signed-in user. Output of the backend’s containers lands in the same source. A line on stdout is info, a line on stderr a warning, and a line that is a JSON object with a stringlevel(debugortrace,info,warnorwarning,error,fatalorcritical) keeps that level. Such a JSON line shows itsmessage(ormsg) as the message and its other fields (at most 32, each cut to 1024 characters) as data; a W3Ctraceparentfield ties it to the request, so searching the request id finds it next to the daemon’s lines. Adebugline is kept only while an administrator turned debug messages on for the app. So log errors of a backend to stderr or as JSON with alevel; plainconsole.logoutput is always info. - The SDK collects entries for 250 ms and sends up to 100 at a time.
Limits: a message is cut at 4000 characters; data keeps 20 fields with keys of up to 100 and values of up to 1000 characters, all turned into text. The SDK buffers at most 500 entries and drops newer ones while it is full; the daemon accepts bursts of 200 and then 20 per second for each app, all its users together, drops the rest and reports the dropped count once a minute as one warning. Never log passwords, tokens or file contents.
Errors
Section titled “Errors”PithyxError has code, params, detail and requestId, and toProblem() for the plain object. isProblem(value) tests any value for the shape. Codes come from two places:
- The daemon, unchanged: for example
app.permission.denied,app.storage.too_large,fs.entry.exists,app.token.invalid. The full list with texts is the error reference. - The SDK for failures between the app and the desktop:
sdk.method.unknown,sdk.request.invalid,sdk.request.timeout,sdk.transport.closed,sdk.protocol.unsupported,sdk.transfer.cancelled,sdk.backend.endpoint_unknownandsdk.backend.unreachable. The list is exported assdkErrorCodes.
app.backend.fetch resolves for every HTTP status. The proxy’s own failures (401, 503) carry a problem body with a code; turn them into a PithyxError as the example does. What your backend returns is up to you.
Retry or not
Section titled “Retry or not”| Code | Meaning | Retry |
|---|---|---|
sdk.request.timeout |
the desktop did not answer within 15 seconds (not for transfers and dialogs) | a few times, with a growing pause |
sdk.backend.unreachable |
the request did not reach the box | after connectionChange with online: true |
app.backend.unavailable |
the backend is updating, failed, or did not start in 30 s | when backendStatus says running, or later |
auth.rate.limited |
too many tokens at once | after params.retryAfterSeconds |
app.token.invalid |
session ended or access to the app gone (the SDK already tried a new token once) | no; the user reopens the window |
app.permission.denied |
the app lacks params.permission |
no; wait for permissionsChange |
sdk.transport.closed |
no desktop connected, or the connection to it is gone | no |
sdk.method.unknown |
the desktop does not serve this call | no; check app.supports first |
sdk.request.invalid |
the parameters are wrong, for example a key over 128 characters | no; a bug in the app |
fs.*, app.storage.* |
the request itself is refused | no; tell the user |
A timed-out call may still have happened on the box. Retry only calls that are safe to repeat, such as reads and upload with overwrite.
Showing errors
Section titled “Showing errors”The app translates the codes it expects with its own texts, using params for the placeholders, and falls back to a general message. detail is English and meant for logs. Log the requestId instead of showing it: it lets an administrator find the request in the Log Center.
app.ui.showError(error) shows the desktop’s own error dialog with the translated text. The desktop translates the daemon’s and the SDK’s codes; for a code of your own it shows the error’s detail, so give it one in the user’s language (new PithyxError({ code: 'lists.list.full', params: {}, detail: t('errors.listFull') })). The request id goes to the browser console, never into the dialog. Check app.supports('ui.showError') as the example does: an older desktop may not serve it and answers sdk.method.unknown. The mock desktop of pithyx dev serves it and writes the code to its log.
Edge cases
Section titled “Edge cases”app.logbeforeconnect()resolved is not possible: there is noappyet. Useconsoleuntil then.- Entries reach the daemon only if the desktop serves
log.write; otherwise they stay in the console. connect()itself rejects withsdk.transport.closedwhen no desktop connects within 10 seconds, for example when the page is opened directly in a tab, and withsdk.protocol.unsupportedwhen the desktop does not speak the SDK’s protocol.