一键导入
error-handling
Error handling with wellcrafted trySync/tryAsync and toastOnError. Use for try-catch, Result types, error toasts, HTTP errors.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Error handling with wellcrafted trySync/tryAsync and toastOnError. Use for try-catch, Result types, error toasts, HTTP errors.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | error-handling |
| description | Error handling with wellcrafted trySync/tryAsync and toastOnError. Use for try-catch, Result types, error toasts, HTTP errors. |
| metadata | {"author":"epicenter","version":"2.0"} |
Use this pattern when you need to:
try-catch blocks with trySync or tryAsync.Ok(...) and propagate failures with Err(...).cause for typed domain error constructors.Load these on demand based on what you're working on:
toastOnError, extractErrorMessage in UI), read references/toast-on-error.mddefineQuery / defineMutation : when to throw vs. return Err, how remote callers see your errors, ActionFailed semantics), read ../workspace-api/references/action-return-shapes.mdWhen handling errors that can be gracefully recovered from, use trySync (for synchronous code) or tryAsync (for asynchronous code) from wellcrafted instead of traditional try-catch blocks. This provides better type safety and explicit error handling.
Related Skills: See
services-layerskill fordefineErrorspatterns and service architecture. Seequery-layerskill for RPC error pass-through and report-boundary presentation.
import { trySync, tryAsync, Ok, Err } from 'wellcrafted/result';
// SYNCHRONOUS: Use trySync for sync operations
const { data, error } = trySync({
try: () => {
const parsed = JSON.parse(jsonString);
return validateData(parsed); // Automatically wrapped in Ok()
},
catch: (e) => {
// Gracefully handle parsing/validation errors
console.log('Using default configuration');
return Ok(defaultConfig); // Return Ok with fallback
},
});
// ASYNCHRONOUS: Use tryAsync for async operations
await tryAsync({
try: async () => {
const child = new Child(session.pid);
await child.kill();
console.log(`Process killed successfully`);
},
catch: (e) => {
// Gracefully handle the error
console.log(`Process was already terminated`);
return Ok(undefined); // Return Ok(undefined) for void functions
},
});
// Both support the same catch patterns
const syncResult = trySync({
try: () => riskyOperation(),
catch: (error) => {
// For recoverable errors, return Ok with fallback value
return Ok('fallback-value');
// For unrecoverable errors, pass the raw cause : the constructor handles extractErrorMessage
return CompletionError.ConnectionFailed({ cause: error });
},
});
trySync for synchronous code, tryAsync for asynchronous codeT, the catch should return Ok<T> for graceful handlingOk(undefined) in the catchErr when you want to propagate the errorcause: unknown and let the defineErrors constructor call extractErrorMessage(cause) inside its message template. Don't call extractErrorMessage at the call site. This centralizes message extraction where the message is composed:// ✅ GOOD: cause: error at call site, extractErrorMessage in constructor
catch: (error) => CompletionError.ConnectionFailed({ cause: error })
// ❌ BAD: extractErrorMessage at call site, string passed to constructor
catch: (error) => CompletionError.ConnectionFailed({ underlyingError: extractErrorMessage(error) })
{ data, error } from tryAsync/trySync, the error variable is the raw error value, NOT wrapped in Err. You must wrap it before returning:// WRONG - error is just the raw error value, not a Result
const { data, error } = await tryAsync({...});
if (error) return error; // TYPE ERROR: Returns raw error, not Result
// CORRECT - wrap with Err() to return a proper Result
const { data, error } = await tryAsync({...});
if (error) return Err(error); // Returns Err<CustomError>
This is different from returning the entire result object:
// This is also correct - userResult is already a Result type
const userResult = await tryAsync({...});
if (userResult.error) return userResult; // Returns the full Result
Whispering $lib/rpc adapters preserve tagged errors from services and operations. They should return Err(error) or the operation result directly, not convert errors into user-facing { title, description } objects.
const { data, error } = await services.blobs.audio.getBlob(recording.id);
if (error) return Err(error);
return Ok(data);
Presentation happens at the UI or operation boundary:
if (error) {
report.error({ cause: error });
return;
}
// SYNCHRONOUS: JSON parsing with fallback
const { data: config } = trySync({
try: () => JSON.parse(configString),
catch: (e) => {
console.log('Invalid config, using defaults');
return Ok({ theme: 'dark', autoSave: true });
},
});
// SYNCHRONOUS: File system check
const { data: exists } = trySync({
try: () => fs.existsSync(path),
catch: () => Ok(false), // Assume doesn't exist if check fails
});
// ASYNCHRONOUS: Graceful process termination
await tryAsync({
try: async () => {
await process.kill();
},
catch: (e) => {
console.log('Process already dead, continuing...');
return Ok(undefined);
},
});
// ASYNCHRONOUS: File operations with fallback
const { data: content } = await tryAsync({
try: () => readFile(path),
catch: (e) => {
console.log('File not found, using default');
return Ok('default content');
},
});
// EITHER: Error propagation (works with both)
// Pass the raw caught error as cause : the defineErrors constructor calls extractErrorMessage
const { data, error } = await tryAsync({
try: () => criticalOperation(),
catch: (error) =>
CompletionError.ConnectionFailed({ cause: error }),
});
if (error) return Err(error);
error explicitlyWhen reading a Result<T, E> that a library (or your own code) returns
: like table.get(id), tryAsync(...), or a service method : always
destructure both data and error and check error on its own line,
even when both paths should produce the same action.
// ✅ GOOD : error is destructured and checked explicitly
const { data: row, error } = table.get(id);
if (error) {
console.warn('[context] corrupted row:', error.message);
return null;
}
if (row === null) return null; // legitimate absence
use(row); // row: TRow
// ❌ BAD : relies on "data is null if error exists" by coincidence
const { data: row } = table.get(id); // error silently swallowed
if (row === null) return null;
use(row);
Why:
if (error) line tells future readers the
error case is considered, not forgotten.Result, but relying on that fact at the
call site ties your code to the representation, not the contract.If both cases genuinely produce the same action (no log, no toast,
no retry, no distinction worth writing down), one combined condition
is fine : as long as error is still destructured:
// ✅ OK : error destructured, both cases deliberately collapsed
const { data: row, error } = table.get(id);
if (error || row === null) continue; // skip in both cases
use(row);
The destructure matters; it signals you thought about the error case
and chose to collapse it. The anti-pattern is destructuring only
data and hoping for the best.
Split into two explicit checks when the handling differs:
const { data: row, error } = table.get(id);
if (error) {
logger.warn('row corrupted, replacing', { id, error });
await replaceWithDefault(id);
return;
}
if (row === null) {
await createMissingRow(id);
return;
}
use(row);
This is the form to prefer by default : collapse back only when there's truly nothing distinct to say.
Use trySync when:
Use tryAsync when:
Use traditional try-catch when:
Typed errors are structured values, so they're also what the wellcrafted/logger wants. log.warn / log.error take a typed error unary : no message argument, no format string. The error owns its message, and the log sink gets the full object (name, fields, cause) alongside it.
Mint the typed error inside catch:, then branch on the Result and log inside the branch. The caller picks the level (.warn for recoverable, .error for loud) at the call site : matching Rust's tracing::warn!(?err) convention, where level lives at the call site and never on the error variant.
import { createLogger } from 'wellcrafted/logger';
import { trySync } from 'wellcrafted/result';
const log = createLogger('sqlite-writer');
const walResult = trySync({
try: () => db.query('PRAGMA journal_mode = WAL').get(),
catch: (cause) => SqliteWriterError.PragmaSetupFailed({ pragma: 'WAL', cause }),
});
if (walResult.error !== null) {
log.warn(walResult.error);
} else if (walResult.data !== 'wal') {
log.warn(SqliteWriterError.WalSilentFallback({ actualMode: walResult.data }));
}
Most epicenter call sites need the Ok branch's data locally, so they branch first and log inside the branch. The mint-and-log shorthand works the same way inside a .catch tail when there's no Result to branch on:
}).catch((cause) => {
log.warn(MaterializerWriteError.TableWriteFailed({ tableName, cause }));
});
log.warn / log.error accept either the raw tagged error (result.error after narrowing) or the Err-wrapped factory output (MyError.Variant({ ... })) and unwrap structurally.
For the rarer Result-chain shape (tryAsync(...).then(...) where the Result flows out of the function), tapErr(log.warn) from wellcrafted/result is the combinator : see the logging SKILL's See also section.
log.error(message, error)?Level is context-dependent (same error can be warn on a retry, error on the last attempt) and message lives on the error variant. That's the whole point of defineErrors : the variant's message: template encodes the "what operation failed" clause. Duplicating it at the call site would drift and rot.
memorySinkNever assert on console output. Use memorySink() and inspect the events array directly:
import { createLogger, memorySink } from 'wellcrafted/logger';
test('logs a warning when the materializer write fails', () => {
const { sink, events } = memorySink();
const log = createLogger('sqlite-materializer', sink);
// ... trigger the path ...
expect(events).toContainEqual(
expect.objectContaining({
level: 'warn',
data: expect.objectContaining({ name: 'TableWriteFailed' }),
}),
);
});
See the logging skill for level semantics, sink composition, and the JSONL file sink.
Run a continuous collapse-and-simplify pass that surgically removes indirection failing to earn its boundary. Use when the user says 'collapse pass', 'simplify pass', 'reduce indirection', 'shrink the surface', 'find what to delete', when asking to audit a package for dead abstractions, when reviewing a pull request, branch, or recent merged change for simplification (isolated in a worktree), or when the goal is a sequence of small refactor commits that delete more than they add. Pairs with code-audit (smell catalog), refactoring (per-change mechanics), one-sentence-test (cohesion gate), cohesive-clean-breaks (deep redesigns), approachability-audit (first-read sanity), and post-implementation-review (second-read after each commit).
How a workspace-backed app under `apps/*` is composed: the isomorphic doc factory (`create<App>`), the environment factories (`open<App>Browser` / `open<App>Extension` / tauri), the `#platform/*` build-time platform DI for multi-platform (Tauri) apps, the `session` singleton, daemon/script placement under per-project `workspaces/<app>/`, and the file layout itself. Use when creating a new app, naming or placing the iso/browser/extension factory, wiring `#platform/*` subpath imports for a Tauri seam, choosing between auth-gated (Shape A) vs module-singleton (Shape B), placing the session singleton, or registering daemon/script bindings.
Query/RPC layer with TanStack Query, defineKeys, service composition, runtime DI. Use for createQuery, createMutation, queries/mutations, reactive data management.
TanStack AI patterns for @tanstack/ai, @tanstack/ai-svelte, chat state, streamed responses, UIMessage parts, tool calling, tool approvals, and provider model adapters. Use when working on AI chat, createChat, fetchServerSentEvents, UIMessage conversion, or TanStack AI tools.
Create a slash-command `/goal` for long-running Codex or Claude Code work when the user explicitly asks for a `/goal`, agent goal, or completion condition. Outputs one goal line with the objective, starting context, validation evidence, and stop condition.
List 3-5 comparable apps when planning a UX surface to test category fit and surface high-leverage refusals. Use for "what do other apps do", identity, sync state, local-first design.