error-handling

安装量: 66
排名: #11540

安装

npx skills add https://github.com/epicenterhq/epicenter --skill error-handling

Error Handling with wellcrafted trySync and tryAsync Use trySync/tryAsync Instead of try-catch for Graceful Error Handling

When 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-layer skill for createTaggedError patterns. See query-layer skill for error transformation to WhisperingError.

The Pattern 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, return Err return ServiceErr({ message: 'Operation failed', cause: error, }); }, });

Key Rules Choose the right function - Use trySync for synchronous code, tryAsync for asynchronous code Always await tryAsync - Unlike try-catch, tryAsync returns a Promise and must be awaited trySync returns immediately - No await needed for synchronous operations Match return types - If the try block returns T, the catch should return Ok for graceful handling Use Ok(undefined) for void - When the function returns void, use Ok(undefined) in the catch Return Err for propagation - Use custom error constructors that return Err when you want to propagate the error CRITICAL: Wrap destructured errors with Err() - When you destructure { 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 TaggedError, not a Result const { data, error } = await tryAsync({...}); if (error) return error; // TYPE ERROR: Returns TaggedError, not Result

// CORRECT - wrap with Err() to return a proper Result const { data, error } = await tryAsync({...}); if (error) return Err(error); // Returns Err

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

Examples // 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) const { data, error } = await tryAsync({ try: () => criticalOperation(), catch: (error) => ServiceErr({ message: 'Critical operation failed', cause: error, }), }); if (error) return Err(error);

When to Use trySync vs tryAsync vs try-catch

Use trySync when:

Working with synchronous operations (JSON parsing, validation, calculations) You need immediate Result types without promises Handling errors in synchronous utility functions Working with filesystem sync operations

Use tryAsync when:

Working with async/await operations Making network requests or database calls Reading/writing files asynchronously Any operation that returns a Promise

Use traditional try-catch when:

In module-level initialization code where you can't await For simple fire-and-forget operations When you're outside of a function context When integrating with code that expects thrown exceptions Wrapping Patterns: Minimal vs Extended The Minimal Wrapping Principle

Wrap only the specific operation that can fail. This captures the error boundary precisely and makes code easier to reason about.

// ✅ GOOD: Wrap only the risky operation const { data: stream, error: streamError } = await tryAsync({ try: () => navigator.mediaDevices.getUserMedia({ audio: true }), catch: (error) => DeviceStreamServiceErr({ message: Microphone access failed: ${extractErrorMessage(error)}, }), });

if (streamError) return Err(streamError);

// Continue with non-throwing operations const mediaRecorder = new MediaRecorder(stream); mediaRecorder.start();

// ❌ BAD: Wrapping too much code const { data, error } = await tryAsync({ try: async () => { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mediaRecorder = new MediaRecorder(stream); mediaRecorder.start(); await someOtherAsyncCall(); return processResults(); }, catch: (error) => GenericErr({ message: 'Something failed' }), // Too vague! });

The Immediate Return Pattern

Return errors immediately after checking. This creates clear control flow and prevents error nesting.

// ✅ GOOD: Check and return immediately const { data: devices, error: enumerateError } = await enumerateDevices(); if (enumerateError) return Err(enumerateError);

const { data: stream, error: streamError } = await getStreamForDevice( devices[0], ); if (streamError) return Err(streamError);

// Happy path continues cleanly return Ok(stream);

// ❌ BAD: Nested error handling const { data: devices, error: enumerateError } = await enumerateDevices(); if (!enumerateError) { const { data: stream, error: streamError } = await getStreamForDevice( devices[0], ); if (!streamError) { return Ok(stream); } else { return Err(streamError); } } else { return Err(enumerateError); }

When to Extend the Try Block

Sometimes it makes sense to include multiple operations in a single try block:

Atomic operations - When operations must succeed or fail together Same error type - When all operations produce the same error category Cleanup logic - When you need to clean up on any failure // Extended block is appropriate here - all operations are part of "starting recording" const { data: mediaRecorder, error: recorderError } = trySync({ try: () => { const recorder = new MediaRecorder(stream, { bitsPerSecond: bitrate }); recorder.addEventListener('dataavailable', handleData); recorder.start(TIMESLICE_MS); return recorder; }, catch: (error) => RecorderServiceErr({ message: Failed to initialize recorder: ${extractErrorMessage(error)}, }), });

Real-World Examples from the Codebase

Minimal wrap with immediate return:

// From device-stream.ts async function getStreamForDeviceIdentifier( deviceIdentifier: DeviceIdentifier, ) { return tryAsync({ try: async () => { const stream = await navigator.mediaDevices.getUserMedia({ audio: { ...constraints, deviceId: { exact: deviceIdentifier } }, }); return stream; }, catch: (error) => DeviceStreamServiceErr({ message: Unable to connect to microphone. ${extractErrorMessage(error)}, }), }); }

Multiple minimal wraps with immediate returns:

// From navigator.ts startRecording: async (params, { sendStatus }) => { if (activeRecording) { return RecorderServiceErr({ message: 'Already recording.' }); }

// First try block - get stream const { data: streamResult, error: acquireStreamError } = await getRecordingStream({ selectedDeviceId, sendStatus }); if (acquireStreamError) return Err(acquireStreamError);

const { stream, deviceOutcome } = streamResult;

// Second try block - create recorder const { data: mediaRecorder, error: recorderError } = trySync({ try: () => new MediaRecorder(stream, { bitsPerSecond: bitrate }), catch: (error) => RecorderServiceErr({ message: Failed to initialize recorder. ${extractErrorMessage(error)}, }), });

if (recorderError) { cleanupRecordingStream(stream); // Cleanup on failure return Err(recorderError); }

// Happy path continues... mediaRecorder.start(TIMESLICE_MS); return Ok(deviceOutcome); },

Summary: Wrapping Guidelines Scenario Approach Single risky operation Wrap just that operation Sequential operations Wrap each separately, return immediately on error Atomic operations that must succeed together Wrap together in one block Different error types needed Separate blocks with appropriate error types Need cleanup on failure Wrap, check error, cleanup if needed, return

The goal: Each trySync/tryAsync block should represent a single "unit of failure" with a specific, descriptive error message.

返回排行榜