Are you tired of writing nested try-catch blocks in your async JavaScript code? Do you find yourself repeating error handling logic across your codebase? Let me introduce you to Trywrap - a lightweight utility module that makes error handling in asynchronous functions clean, flexible, and maintainable.

The Problem with Traditional Error Handling

Let's look at a typical scenario where we need to handle errors in multiple async operations:

async function complexOperation() {
    try {
        // Operation 1
        try {
            await fetchUserData();
        } catch (error) {
            console.error("Failed to fetch user data:", error);
            // Handle error...
        }

        // Operation 2
        try {
            await processData();
        } catch (error) {
            console.error("Failed to process data:", error);
            // Handle error...
        }

        // Operation 3
        try {
            await saveResults();
        } catch (error) {
            console.error("Failed to save results:", error);
            // Handle error...
        }
    } catch (error) {
        console.error("Unexpected error:", error);
        // Handle any uncaught errors...
    }
}

This code is:

  • 😫 Verbose and repetitive
  • 😕 Hard to maintain
  • 🤔 Difficult to standardize error handling
  • 😬 Prone to inconsistencies

Enter Trywrap

Trywrap provides a elegant solution to this problem. Here's how you can transform the above code:

const trywrap = require('trywrap');

async function complexOperation() {
    const onError = ({ error, methodName }) => {
        console.error(`Error in ${methodName}:`, error.message);
    };

    await trywrap(fetchUserData, [], { 
        onError, 
        fallback: defaultUserData 
    });

    await trywrap(processData, [], { 
        onError, 
        fallback: [] 
    });

    await trywrap(saveResults, [], { 
        onError,
        fallback: false 
    });
}

🚀 Key Features

  1. Centralized Error Handling: Define error handling logic once and reuse it across your application.
  2. Fallback Values: Specify default values to return when operations fail.
  3. Flexible Configuration: Customize error handling behavior with options like rethrow.
  4. Detailed Error Context: Access method names and arguments in error handlers for better debugging.

📦 Installation

npm install trywrap

🛠️ How to Use Trywrap

Basic Usage

const trywrap = require('trywrap');

async function riskyOperation(value) {
    if (value < 0) throw new Error("Invalid value");
    return value * 2;
}

// Define an error handler
const onError = ({ error, methodName, args }) => {
    console.error(`Error in ${methodName}:`, error.message);
};

// Use trywrap
const result = await trywrap(riskyOperation, [10], {
    onError,
    fallback: 0
});

Advanced Usage: API Calls

async function fetchUserProfile(userId) {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) throw new Error('User not found');
    return response.json();
}

const userProfile = await trywrap(fetchUserProfile, ['123'], {
    onError: ({ error }) => {
        // Log to monitoring service
        logError(error);
    },
    fallback: { 
        id: '123',
        name: 'Guest User',
        isGuest: true
    }
});

🎯 When to Use Trywrap

Trywrap is perfect for:

  1. API Integration: Handle network requests and response parsing
  2. Database Operations: Manage database query errors gracefully
  3. File System Operations: Handle file read/write errors
  4. Any async operations where you want consistent error handling

💡 Pro Tips

  1. Create Reusable Error Handlers:
const defaultErrorHandler = ({ error, methodName }) => {
    logger.error(`${methodName} failed:`, error);
    metrics.incrementError(methodName);
};
  1. Use Type-Specific Fallbacks:
const arrayFallback = [];
const objectFallback = null;
const booleanFallback = false;

// Use appropriate fallbacks
await trywrap(fetchUsers, [], { fallback: arrayFallback });
await trywrap(fetchUser, [id], { fallback: objectFallback });
await trywrap(validateUser, [data], { fallback: booleanFallback });
  1. Chain Operations:
const processUser = async (userId) => {
    const user = await trywrap(fetchUser, [userId], {
        onError,
        fallback: null
    });

    if (!user) return;

    const processed = await trywrap(processUserData, [user], {
        onError,
        fallback: user
    });

    return processed;
};

🎉 Benefits

  • Cleaner Code: No more nested try-catch blocks
  • DRY Principle: Define error handling once, use everywhere
  • Better Maintainability: Standardized error handling across your codebase
  • Flexible: Customize behavior with options and fallbacks
  • Lightweight: Minimal overhead, maximum impact

🔍 Real-World Example

Here's a practical example showing how Trywrap can improve your code organization:

// Before Trywrap
async function getUserData(userId) {
    try {
        const user = await fetchUser(userId);
        try {
            const posts = await fetchUserPosts(userId);
            try {
                const analytics = await fetchUserAnalytics(userId);
                return { user, posts, analytics };
            } catch (error) {
                console.error('Analytics error:', error);
                return { user, posts, analytics: null };
            }
        } catch (error) {
            console.error('Posts error:', error);
            return { user, posts: [], analytics: null };
        }
    } catch (error) {
        console.error('User error:', error);
        return null;
    }
}

// After Trywrap
async function getUserData(userId) {
    const onError = ({ error, methodName }) => {
        console.error(`${methodName} failed:`, error);
    };

    const user = await trywrap(fetchUser, [userId], {
        onError,
        fallback: null
    });

    if (!user) return null;

    const posts = await trywrap(fetchUserPosts, [userId], {
        onError,
        fallback: []
    });

    const analytics = await trywrap(fetchUserAnalytics, [userId], {
        onError,
        fallback: null
    });

    return { user, posts, analytics };
}

🌟 Conclusion

Trywrap brings elegance and simplicity to error handling in async JavaScript. It helps you write cleaner, more maintainable code while ensuring robust error handling across your application.

Give it a try in your next project:

npm install trywrap

🔗 Useful Links


What's your take on error handling in async JavaScript? Have you faced similar challenges? Share your thoughts and experiences in the comments below! 👇