Enhance Error Handling with a Custom Error Object: A Comprehensive Guide

In a previous post, we discussed using a Result object to encapsulate success and error states in your application. While the Result object is effective for separating success from failure in function outcomes, combining it with a Custom Error Object creates a robust solution for handling errors in a detailed and structured way.

In this post, we’ll cover how a Custom Error Object can be implemented in Python and Node.js to improve error reporting, and why it's beneficial when used alongside the Result object.

Revisiting the Result Object and Its Purpose

In the previous post, we explored the Result object pattern, which is useful for handling both success and failure outcomes in a predictable way. It allows you to return consistent information for both successful and failed operations without resorting to multiple return values or throwing exceptions everywhere.

However, sometimes you need more detailed error information than a simple error message. This is where the Custom Error Object comes into play.

What is a Custom Error Object?

A Custom Error Object is a specialized class that captures detailed information about an error. Instead of just returning an error message, it includes:

  • Process name: Identifies the function or module where the error occurred.
  • Message: Provides a clear description of the error.
  • Error code: Categorizes the error (e.g., 400 for bad request).
  • Details: Additional context for troubleshooting.

By combining a Custom Error Object with a Result object, you not only get the best of both worlds (clean success/error separation and detailed error handling), but also improve your code’s structure and maintainability.

Implementing a Custom Error Object in Python

Here’s how you can implement a Custom Error Object to complement your Result object:

class CustomError:
    def __init__(self, process, message, code, details=None):
        self.process = process
        self.message = message
        self.code = code
        self.details = details or {}

    def to_dict(self):
        return {
            'process': self.process,
            'message': self.message,
            'code': self.code,
            'details': self.details,
        }

class Result:
    def __init__(self, success, data=None, error=None):
        self.success = success
        self.data = data
        self.error = error

    @classmethod
    def success(cls, data):
        return cls(True, data)

    @classmethod
    def error(cls, error):
        return cls(False, error=error)

    def is_success(self):
        return self.success

# Example usage:
def process_data(data):
    try:
        if not data:
            raise ValueError("No data provided")
        return Result.success("Data processed successfully")
    except ValueError as e:
        error = CustomError("process_data", str(e), 400)
        return Result.error(error.to_dict())

# Testing
result = process_data(None)
if result.is_success():
    print(result.data)
else:
    print(result.error)

In this example:

  • The CustomError class encapsulates detailed error information.
  • The Result class provides a standardized way to return either a successful outcome or an error.
  • The two are used together, allowing the function to return both detailed error information and clean success messages.

Implementing a Custom Error Object in Node.js

In Node.js, you can implement a similar pattern:

class CustomError {
    constructor(process, message, code, details = {}) {
        this.process = process;
        this.message = message;
        this.code = code;
        this.details = details;
    }

    toJSON() {
        return {
            process: this.process,
            message: this.message,
            code: this.code,
            details: this.details,
        };
    }
}

class Result {
    constructor(success, data = null, error = null) {
        this.success = success;
        this.data = data;
        this.error = error;
    }

    static success(data) {
        return new Result(true, data);
    }

    static error(error) {
        return new Result(false, null, error);
    }

    isSuccess() {
        return this.success;
    }
}

// Example usage:
async function processData(data) {
    try {
        if (!data) throw new Error("No data provided");
        return Result.success("Data processed successfully");
    } catch (err) {
        const error = new CustomError("processData", err.message, 400, { data });
        return Result.error(error.toJSON());
    }
}

(async () => {
    const result = await processData(null);
    if (result.isSuccess()) {
        console.log(result.data);
    } else {
        console.error(result.error);
    }
})();

In this example:

  • CustomError is used to capture detailed information about the error.
  • Result is used to return either a successful response or an error object, following a consistent return pattern.

Why Use Both the Result Object and Custom Error Object?

Using both a Result object and a Custom Error Object offers several advantages:

Separation of Concerns: The Result object cleanly separates success and failure states. You can easily check if a process succeeded or failed, and handle the result accordingly.

Detailed Error Reporting: The Custom Error Object provides detailed information about the error, including where it occurred, an error code, and any additional details that can help with debugging.

Predictable Outcomes: By using the Result object, you always know what kind of object will be returned—either a success object with data or an error object with detailed error information.

Improved Debugging: With the combination of a Custom Error Object and a Result object, developers can pinpoint issues more efficiently, as errors are logged with detailed information about the context in which they occurred.

Conclusion

By combining a Result object and a Custom Error Object, you can significantly improve the way you handle errors in both Python and Node.js applications. This structured approach ensures consistent error reporting, improved debugging, and cleaner code. Whether you’re building small scripts or large-scale applications, this combination will make your error handling more predictable and scalable.

Incorporating these patterns into your development workflow is a great step toward writing more maintainable, resilient code that can handle success and failure paths gracefully.