tryError Documentation
Common Pitfalls
Learn from common mistakes and avoid gotchas when using tryError.
❌ Wrong Approach
const result = await tryAsync(() => fetchUserData());
if (!isOk(result)) {
// ❌ TypeScript Error: Property 'message' does not exist on type 'never'
console.log(result.message);
console.log(result.type);
}
✅ Correct Approach
const result = await tryAsync(() => fetchUserData());
if (isTryError(result)) {
// ✅ Type-safe access to error properties
console.log(result.message);
console.log(result.type);
console.log(result.source);
}
// Alternative pattern
if (isErr(result)) {
// result is guaranteed to be TryError
handleError(result);
}
Why this happens: TypeScript sees TryResult<T, E>
as a union type. When you check !isOk(result)
, TypeScript can't narrow the type to just the error case. Use isTryError(result)
or isErr(result)
for proper type narrowing.
❌ Over-wrapping Functions
// ❌ Don't wrap every operation
function processUser(user: User) {
const nameResult = trySync(() => user.name.toUpperCase());
const emailResult = trySync(() => user.email.toLowerCase());
const ageResult = trySync(() => user.age + 1);
// This adds unnecessary overhead for simple operations
if (isOk(nameResult) && isOk(emailResult) && isOk(ageResult)) {
return { name: nameResult, email: emailResult, age: ageResult };
}
}
✅ Strategic Use of tryError
// ✅ Only wrap operations that can actually fail
function processUser(user: User) {
// Simple operations that won't throw - no wrapping needed
const name = user.name.toUpperCase();
const email = user.email.toLowerCase();
const age = user.age + 1;
// Wrap the risky validation step
const validationResult = trySync(() => validateUserData({ name, email, age }));
if (isTryError(validationResult)) {
return validationResult; // Return error
}
return { name, email, age }; // Return success
}
// ✅ Or wrap the entire operation
function processUserSafely(user: User) {
return trySync(() => {
const validation = validateUserData(user);
if (!validation.valid) {
throw new Error(validation.message);
}
return {
name: user.name.toUpperCase(),
email: user.email.toLowerCase(),
age: user.age + 1
};
});
}
Performance tip: tryError has zero overhead for the success path, but each wrapper function call still has JavaScript function call overhead. Use it strategically for operations that can actually fail.
❌ Inconsistent Error Handling
// ❌ Mixing try/catch with tryError
async function fetchAndProcess(id: string) {
try {
const userData = await tryAsync(() => fetchUser(id));
if (isTryError(userData)) {
throw new Error(userData.message); // Converting back to exception
}
// Now mixing with regular try/catch
const processed = await processUserData(userData);
return processed;
} catch (error) {
// This catches both thrown errors and any unhandled exceptions
console.error(error);
return null;
}
}
✅ Consistent Error-as-Values
// ✅ Pure tryError approach
async function fetchAndProcess(id: string): Promise<TryResult<ProcessedUser>> {
const userData = await tryAsync(() => fetchUser(id));
if (isTryError(userData)) {
return userData; // Propagate error
}
const processedData = await tryAsync(() => processUserData(userData));
if (isTryError(processedData)) {
return processedData; // Propagate error
}
return processedData; // Return success
}
// ✅ Or use chaining for cleaner code
async function fetchAndProcessChained(id: string): Promise<TryResult<ProcessedUser>> {
return tryChainAsync(
await tryAsync(() => fetchUser(id)),
(userData) => tryAsync(() => processUserData(userData))
);
}
❌ No Loading States
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchUser = async () => {
const result = await tryAsync(() => api.getUser(userId));
if (isTryError(result)) {
setError(result.message);
} else {
setUser(result);
}
};
fetchUser();
}, [userId]);
// ❌ No loading state - user sees blank screen during fetch
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>; // This shows before loading
return <UserDisplay user={user} />;
}
✅ Proper Loading States
function UserProfile({ userId }: { userId: string }) {
const [state, setState] = useState<{
user: User | null;
loading: boolean;
error: TryError | null;
}>({
user: null,
loading: true,
error: null
});
useEffect(() => {
const fetchUser = async () => {
setState(prev => ({ ...prev, loading: true, error: null }));
const result = await tryAsync(() => api.getUser(userId));
if (isTryError(result)) {
setState({ user: null, loading: false, error: result });
} else {
setState({ user: result, loading: false, error: null });
}
};
fetchUser();
}, [userId]);
// ✅ Clear loading states and error handling
if (state.loading) return <LoadingSpinner />;
if (state.error) return <ErrorDisplay error={state.error} />;
if (!state.user) return <NotFound />;
return <UserDisplay user={state.user} />;
}
// ✅ Even better: use the useTry hook
function UserProfileWithHook({ userId }: { userId: string }) {
const { data: user, error, loading } = useTry(
() => api.getUser(userId),
[userId]
);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorDisplay error={error} />;
if (!user) return <NotFound />;
return <UserDisplay user={user} />;
}
❌ All-or-Nothing Approach
// ❌ Fails completely if any part fails
async function loadDashboardData() {
const userResult = await tryAsync(() => fetchUser());
const postsResult = await tryAsync(() => fetchUserPosts());
const settingsResult = await tryAsync(() => fetchUserSettings());
// If any fails, the whole operation fails
if (isTryError(userResult) || isTryError(postsResult) || isTryError(settingsResult)) {
return createError({ type: 'DashboardError', message: 'Failed to load dashboard' });
}
return { user: userResult, posts: postsResult, settings: settingsResult };
}
✅ Graceful Partial Failures
// ✅ Handle partial failures gracefully
async function loadDashboardData() {
const [userResult, postsResult, settingsResult] = await Promise.all([
tryAsync(() => fetchUser()),
tryAsync(() => fetchUserPosts()),
tryAsync(() => fetchUserSettings())
]);
// User data is critical - fail if missing
if (isTryError(userResult)) {
return userResult;
}
// Posts and settings are optional - provide defaults
const posts = isTryError(postsResult) ? [] : postsResult;
const settings = isTryError(settingsResult) ? getDefaultSettings() : settingsResult;
return {
user: userResult,
posts,
settings,
errors: {
posts: isTryError(postsResult) ? postsResult : null,
settings: isTryError(settingsResult) ? settingsResult : null
}
};
}
❌ Generic Error Messages
// ❌ Generic, unhelpful errors
async function updateUserProfile(userId: string, data: UserUpdate) {
const result = await tryAsync(() =>
fetch(`/api/users/${userId}`, {
method: 'PUT',
body: JSON.stringify(data)
})
);
if (isTryError(result)) {
// Generic error - hard to debug
return createError({ type: 'UpdateError', message: 'Update failed' });
}
return result.json();
}
✅ Rich Error Context
// ✅ Rich error context for better debugging
async function updateUserProfile(userId: string, data: UserUpdate) {
const result = await tryAsync(() =>
fetch(`/api/users/${userId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
);
if (isTryError(result)) {
return createError({
type: 'NetworkError',
message: `Failed to update user profile: ${result.message}`,
context: {
userId,
operation: 'updateProfile',
endpoint: `/api/users/${userId}`,
requestData: data,
originalError: result
}
});
}
if (!result.ok) {
const errorData = await result.text();
return createError({
type: 'ApiError',
message: `Server error: ${result.status} ${result.statusText}`,
context: {
userId,
status: result.status,
statusText: result.statusText,
responseBody: errorData,
endpoint: `/api/users/${userId}`
}
});
}
return result.json();
}
❌ Unsafe Context Access
const result = await tryAsync(() => validateUserInput(data));
if (isTryError(result)) {
// ❌ TypeScript Error: Property 'field' does not exist on type 'unknown'
console.log(result.context.field);
// ❌ Runtime Error: Cannot read properties of undefined
console.log(result.context.validationRules);
}
✅ Type-Safe Context Access
// Define your context interface
interface ValidationContext {
field: string;
value: unknown;
rule: string;
}
// Type guard for validation
function isValidationContext(context: unknown): context is ValidationContext {
return typeof context === 'object' &&
context !== null &&
'field' in context &&
'rule' in context &&
typeof (context as any).field === 'string';
}
const result = await tryAsync(() => validateUserInput(data));
if (isTryError(result)) {
// ✅ Safe context access with type checking
if (isValidationContext(result.context)) {
console.log(`Validation failed for field: ${result.context.field}`);
console.log(`Rule: ${result.context.rule}`);
console.log(`Value: ${result.context.value}`);
} else {
console.log('Error occurred but context format is unknown');
}
}
✅ Simplified Pattern for Known Context
const result = await tryAsync(() => validateUserInput(data));
if (isTryError(result)) {
// ✅ Quick check with fallback
const context = result.context as { field?: string; rule?: string } | undefined;
if (context?.field && context?.rule) {
console.log(`Validation failed: ${context.field} (${context.rule})`);
} else {
console.log(`Validation failed: ${result.message}`);
}
}
✅ Generic Context Helper
// Utility function for safe context access
function getContextValue<T>(
error: TryError,
key: string,
defaultValue: T
): T {
if (typeof error.context === 'object' &&
error.context !== null &&
key in error.context) {
return (error.context as any)[key] ?? defaultValue;
}
return defaultValue;
}
// Usage
const result = await tryAsync(() => validateUserInput(data));
if (isTryError(result)) {
const field = getContextValue(result, 'field', 'unknown');
const rule = getContextValue(result, 'rule', 'unknown');
console.log(`Validation failed for ${field} (rule: ${rule})`);
}
Why context is unknown: Error context is intentionally typed as unknown
because it can contain any data structure depending on where the error originated. This forces you to validate the shape before accessing properties, preventing runtime errors from unexpected context structures.
Best practices for context:
• Always validate context structure before accessing properties
• Create type guards for your common context patterns
• Provide fallback values when context is missing or malformed
• Document your error context interfaces for team consistency
✅ Best Practices
- • Use
isTryError()
orisErr()
for type narrowing - • Only wrap operations that can actually fail
- • Maintain consistent error handling throughout your app
- • Always show loading states in UI components
- • Handle partial failures gracefully when possible
- • Include rich context in error objects
- • Validate error context before accessing properties
- • Use error boundaries for unexpected errors
- • Provide retry mechanisms for transient failures
❌ Common Mistakes
- • Accessing error properties without type guards
- • Wrapping every single function call
- • Mixing try/catch with tryError paradigms
- • Forgetting to handle loading states
- • Using all-or-nothing error handling
- • Creating generic, unhelpful error messages
- • Accessing error context without type validation
- • Not testing error paths in your code
- • Ignoring TypeScript strict mode warnings