DEV Community

Christian César Robledo López
Christian César Robledo López

Posted on • Updated on • Originally published at masquerade-circus.net

Integral Error-Data Duality (IEDD)

A rethink of error handling and data representation in programming.

The Integral Error-Data Duality (IEDD) paradigm advocates for treating errors with the same level of importance as regular data in software applications. Central to IEDD is the notion that errors aren't just anomalies but vital pieces of information that should be handled systematically and predictably because errors and valid data are equally important and intrinsic aspects of computational results. By integrating errors seamlessly into the data flow, IEDD aims to enhance clarity, predictability, and resilience in software design and development. This approach encourages us to proactively address and represent errors, ensuring robust and intuitive software solutions.

Introduction

Traditional error handling mechanisms have often been piecemealed across different languages, leading to fragmented and inconsistent code patterns. Consider the vast landscape of contemporary programming languages:

C#: Utilizes the "try-catch-finally" paradigm, where exceptions are thrown and must be caught in dedicated blocks of code. While this promotes explicit error handling, it sometimes results in verbose code and may obfuscate the primary logic.

Node.js (JavaScript): Traditionally, JavaScript has always incorporated the try-catch mechanism for synchronous error handling, however, because of the asynchronous nature of many operations, earlier versions relied heavily on callback functions where the first parameter typically represented an error (aka Error-First Callbacks). With the advent of Promises error handling shifted towards .catch() blocks, and with the introduction of async/await there was a shift back towards the try-catch mechanism to catch errors in asynchronous functions.

Python: Uses a similar "try-except-finally" paradigm as C#. While it's Pythonic to "ask for forgiveness, not permission" (meaning it's more common to handle errors after attempting an operation rather than preemptively checking), this can lead to multiple layers of nested error handling.

Go: Introduces a novel approach by often returning two values from functions that can result in errors: the result and the error. If the error is non-nil, it's understood that the operation failed. This approach somewhat aligns with the principles of IEDD (they inspire it) and is a step in the right direction for IEDD, however, developers should embrace the principles of IEDD to fully benefit from it.

Due to these varied approaches to error handling, IEDD seeks to provide a consolidated, intuitive, and scalable mechanism by elevating error representation to the same level of importance as data, fostering more intuitive, secure, and consistent programming practices.

The Principles of IEDD

The Integral Error-Data Duality (IEDD) paradigm introduces a fresh perspective on error handling and data representation by espousing a set of fundamental principles:

Unified Representation

In IEDD, the concept of a unified representation advocates for a consistent convention where functions or methods return both data and errors in every scenario. This could be looked like the duality null-error for error cases and value-null for valid ones. This approach ensures a clear and predictable code structure, allowing to easily discern the outcome of any operation by streamlining logic pathways.

Equal Importance of Data and Error

IEDD posits that errors are not just unwanted or unexpected outcomes; they are equally as vital as successful data results. By giving the same importance to errors, there is an encouragement to handle and contemplate them with greater emphasis, leading to more resilient and secure systems.

Predictable Error Propagation

With a standard representation for errors integrated into the data structure, the propagation of errors becomes more predictable. Systems can be designed to consistently pass and escalate errors, ensuring they're adequately handled at the appropriate level.

Implementing IEDD

The concept of IEDD and its principles are universal. IEDD can be adopted and implemented across various languages and platforms, making it a versatile approach that transcends specific development ecosystems.

However, implementing the IEDD paradigm requires a shift in mindset, viewing data and errors with equal importance. To embrace the principles of IEDD in your software, it's essential to adjust the way functions handle and return data and errors.

Pseudocode Example

Traditional Error Handling

The next example in pseudocode is performing a validation of user data. It checks the validity of the username and password. If either of them is empty or null, it throws an exception.

function validateUserData(username, password):
    if isNullOrEmpty(username):
        throw "Username cannot be empty"
    endif

    if isNullOrEmpty(password):
        throw "Password cannot be empty"
    endif

    return true
end function

# Usage:
try:
    isValid = validateUserData("user", "pass")
    # Use isValid if no exception is thrown
    # Perform another task in the workflow that can throw an exception
    # And another task that can throw an exception
    # And so on...
catch exception:
    # Handle any exception thrown by any of the workflow tasks
endtry
Enter fullscreen mode Exit fullscreen mode

This is the typical "interrupt" style of error handling, where all the logic is encapsulated in a huge try-catch block.

In the usage, if the function or any of the subsequent tasks encounters an error, it interrupts the normal flow of the program and jumps to the catch block. Then the developer needs to decide how to check the type of error and how to handle it.

IEDD Error Handling

This is the same example but using IEDD principles.

function validateUserData(username, password):
    if isNullOrEmpty(username):
        return (null, "Username cannot be empty")
    end if

    if isNullOrEmpty(password):
        return (null, "Password cannot be empty")
    end if

    return (true, null)
end function

# Usage:
(isValid, error) = validateUserData("user", "pass")

if error != null:
    # In here we inherently know that the error came from the 
    # validateUserData function
    # And do not need to check the type of error or make enforce 
    # a mental model to distinguish between multiple errors 
    # and how to handle them
    # So, we can confidently handle or propagate the error to the caller
else:
    # Continue with the next task in the workflow
end if
Enter fullscreen mode Exit fullscreen mode

Here, instead of "interrupting" the program's flow, the function returns a unified structure (a tuple) that contains both the result and the error. Errors no longer interrupt the flow but are returned just like any other data.

If there is an error, the caller knows exactly where it came from and can handle it accordingly. The workflow continues if there is no error.

Implementing IEDD in Various Languages

Although the IEDD paradigm is language-agnostic and does not enforce a specific implementation, here are some examples of how it can be implemented in various languages.

C

In C#, you can use Tuples or ValueTuple to implement IEDD.

public (bool? isValid, Exception error) ValidateUserData(string username, string password)
{
    if (string.IsNullOrEmpty(username))
    {
        return (null, new Exception("Username cannot be empty"));
    }

    if (string.IsNullOrEmpty(password))
    {
        return (null, new Exception("Password cannot be empty"));
    }

    return (true, null);
}

// Usage:
var (isValid, error) = ValidateUserData("user", "pass");
if (error != null)
{
    // Handle or propagate the error
}
else
{
    // Continue with the next task in the workflow
}
Enter fullscreen mode Exit fullscreen mode

Javascript

While JavaScript doesn't natively support tuples, arrays and destructuring can be used to accomplish a similar effect.

function validateUserData(username, password) {
  if (!username) {
    return [null, new Error("Username cannot be empty")];
  }

  if (!password) {
    return [null, new Error("Password cannot be empty")];
  }

  return [true, null];
}

// Usage:
const [isValid, error] = validateUserData("user", "pass");
if (error) {
  // Handle or propagate the error
} else {
  // Continue with the next task in the workflow
}
Enter fullscreen mode Exit fullscreen mode

Python

Python natively supports tuples, making it straightforward to implement IEDD.

def validate_user_data(username, password):
    if not username:
        return (None, Exception("Username cannot be empty"))
    if not password:
        return (None, Exception("Password cannot be empty"))
    return (True, None)

# Usage:
isValid, error = validate_user_data("user", "pass")
if error:
    # Handle or propagate the error
else:
    # Continue with the next task in the workflow
Enter fullscreen mode Exit fullscreen mode

Go

Go's multiple return values naturally fit into the IEDD paradigm. However, the language's design does not inherently adopt the IEDD principles. By design Go's multiple return values were primarily introduced to manage the results of multiple computations or statements. Only with time it starts to emerge the common pattern to return an error as the second value.

func validateUserData(username string, password string) (bool?, error) {
    if username == "" {
        return nil, errors.New("Username cannot be empty")
    }
    if password == "" {
        return nil, errors.New("Password cannot be empty")
    }
    return true, nil
}

// Usage:
isValid, err := validateUserData("user", "pass")
if err != "" {
    // Handle or propagate the error
} else {
    // Continue with the next task in the workflow
}
Enter fullscreen mode Exit fullscreen mode

As with the other languages that can use data structures capable of returning multiple values (like Javascript arrays), one must be disciplined in applying the IEDD principles, because the abuse of this feature in such structures can lead to anti-patterns.

Advantages of IEDD

Streamlined Code Structure

IEDD offers a uniform way to return both data and errors, making code simpler and more intuitive. Instead of scattering error handling across different places or waiting to be thrown and be caught in some other (many times unknown) place, everything comes in a single package.

Predictability

Because every function or method adheres to the same return pattern (data and error), you always know what to expect. This consistency can significantly speed up both coding and code review processes.

Clearer Communication

When functions return both data and errors, it's immediately clear to the caller if something went wrong and why. There's no need to dive deep into the function's internal logic or documentation to understand potential pitfalls.

Inherent Introspection and Metacognition

With the IEDD approach, errors inherently contain metadata about their nature that explicitly inform about the 'where' and 'why' of their occurrence. Such clarity ensures that systems are built in a more self-aware way, allowing them to dynamically adapt and react to errors based on their specific context and underlying reasons.

Enhances Developing Experience

Reduced cognitive load means that we can focus on the core logic instead of getting bogged down in intricate error handling. This can lead to a smoother development process and more maintainable code.

Enhanced Testing and TDD with IEDD

The IEDD paradigm simplifies testing by eliminating the need for complex exception-triggering scenarios, allowing focus on function return values. This accelerates the testing process and enriches test coverage. Additionally, those practicing Test-Driven Development (TDD) find it easier to develop with tests built for both error and successful scenarios, streamlining their workflow.

Flexibility in Evolving Codebases

As software grows and changes its error-handling needs do it too. With IEDD's unified structure, we can more easily adapt to these changes without having to overhaul the entire error-handling system.

Enhanced System Observability, Debugging, and Monitoring

With a consistent representation of errors and data, monitoring and logging tools can more easily track and report issues. Debugging becomes more straightforward. We can quickly know the origin, nature, and implications of an error, prioritizing and speeding up the troubleshooting process. Monitoring systems can also be optimized to track and alert based on unified error-data structures and their level of implications, enhancing system observability.

IEDD and Pure Functions

Pure functions are a fundamental concept in functional programming. At their core, pure functions are characterized by two primary attributes:

  • Deterministic Outputs: A pure function's output is solely determined by its input values. Given the same inputs, it will consistently produce the same output, regardless of when and how many times it's invoked.

  • No Side Effects: Pure functions do not cause any observable side effects, meaning they don't alter any external state and don't rely on any external variables or states to produce their output.

Integral Error-Data Duality (IEDD) recognizes the unique nature of pure functions and gives the next considerations about them:

  • Error Handling: Pure functions, by design, are shielded from common error scenarios because they operate solely on their inputs without external interference. From the IEDD perspective, errors associated with pure functions are typically the result of issues external to the function at the caller's level. So, there is no need to enforce a pure function to return errors within the IEDD paradigm.

  • Clear Identification is Key: IEDD underscores the need to unmistakably identify pure functions. This can be achieved through different means such as naming conventions, documentation or specific code annotations. The objective is to provide clear signals of its pure nature, ensuring they immediately identify what to expect from the function.

  • Natural Alignment with IEDD: By their very nature of not producing errors or side effects, pure functions inherently align with the principles of IEDD. Their deterministic nature and absence of side effects make them predictable and transparent, qualities highly valued in the IEDD approach. Because pure functions don't return errors, their consistent behavior ensures they align with the IEDD's emphasis on clarity and predictability.

Workflow Errors and Exceptions in the IEDD Paradigm

For IEDD is fundamental to differentiate between two predominant types of anomalies: workflow errors and exceptions. Understanding this distinction ensures that we can effectively leverage the IEDD principles while maintaining a robust and reliable system.

Workflow Errors

Workflow errors are the expected errors that may arise during the normal course of a program's operation. These are scenarios that we can anticipate, such as invalid input, wrong permissions, inexistent entities, request rate limiting, etc. In the IEDD paradigm, these are the errors that are explicitly returned alongside the data, perfectly fitting within the paradigm's notion of treating data and errors with equal importance.

Exceptions

Exceptions, on the other hand, are unanticipated errors that arise due to unexpected conditions in a system. They can come from a plethora of sources, such as memory overflows, null pointer dereferences, or external system crashes. These are the anomalies that traditional try-catch or try-except mechanisms are designed to handle.

The IEDD paradigm does not negate the importance of these mechanisms. While the IEDD approach emphasizes elevating error handling within the predictable workflow, it also acknowledges that the unpredictable nature of exceptions requires a separate handling mechanism.

For example, we can anticipate and handle the case where an entity in the database doesn't exist (a workflow error); in such case, we explicitly will create and return an error to the caller. But, we can't anticipate the case where the database crashes (an exception) because is an external system that we can't control.

Integrating Exceptions into IEDD

When adopting the IEDD paradigm, it's essential to remain vigilant about these unexpected exceptions. Built-in error-handling mechanisms provided by the programming language should still be used to catch these unforeseen challenges. Once caught, however, these exceptions should be handled by applying the IEDD principles.

In practice, this might look like:

result, workflowError = someBusinessLogicFunction()
if workflowError:
    # Handle the expected workflow error as per IEDD
end if

try:
    # This function can throw an exception
    result2 = someExternalSystemFunction(result) 
catch exception:
    # Convert the unexpected exception into a format suitable for IEDD
    errorData = (null, exception)
    # Now handle errorData according to IEDD principles to inform the caller
    # And maybe log or perform other actions based on the exception
end try
# Continue with the next task in the workflow
Enter fullscreen mode Exit fullscreen mode

By maintaining a consistent approach to both workflow errors and unexpected exceptions, developers can provide a cohesive, intuitive, and resilient error-handling experience throughout the software's lifecycle.

Aside Note: Implementing the connection with external systems with a Services Layer or a Repository Pattern can help to isolate the external system's exceptions and handle them with the IEDD principles more easily.

Dealing with Complex Error Hierarchies

As applications grow in complexity, so does the variety and nature of errors they can produce. Recognizing this, the IEDD paradigm suggests the use of an enriched structured error object.

Suggested Structured Error Object

The suggested structured error object should contain (but is not limited nor enforced) the following properties:

  • Message: A clear and descriptive message detailing the error. (Taken/Set usually from the native exception/error object)

  • Stack Trace: A stack trace of the error. (Taken usually from the native exception/error object too)

  • Type: A classification of the error, such as "Validation", "Database", or "Network". This tells us what system or component failed.

  • Severity: An indication of the error's impact, categorized as "Critical", "High", "Medium", or "Low". This tells us what is the impact of the error and can be used to prioritize the error handling or logging.

  • Cause (Optional): Optional property, inspired by the Error:cause property in Javascript. This property tries to give deeper context. Especially beneficial when errors cascade or when an error is a result of another, providing a chain of causality.

  • Metadata (Optional): Optional property to store additional metadata about the error. This can be used to store additional information about the error.

Error Propagation with Context

By using the cause property, we can trace back the lineage of an error, understanding its origin and the sequence of events leading up to it. This is particularly valuable when dealing with layered architectures or external systems.

In such cases where errors can cascade, the cause property can be used to provide context, allowing developers to understand the underlying cause of an error and handle it accordingly.

Additionally, composing the error in this way allows us to give a better message to the user. Ex, if the database fails, we compose an error with the metadata needed by the monitoring system and another error with a message that can be shown to the user.

This allows us to separate the error handling for the user (custom message or notification) and the error data for the internal fixing actions (metadata for the monitoring system tells what system failed, why and the severity of the error).

Real-World like Example

Consider the following example of a user creation workflow. Here, we're using the structured error object to encapsulate and propagate the error information. This allows us to handle the error at the appropriate level.

# Asume severity as the suggested values: Critical, High, Medium, Low
# Asume Error as a native class that gets the stack trace natively

# The optional cause property is used to store the original error 
# that caused the error
function composeError(message, { type, severity, cause?, metadata? }):
    error = new Error(message)
    error.type = type
    error.severity = severity
    error.cause = cause
    error.metadata = metadata
end function

function validateUserData(username, password):
    if isNullOrEmpty(username):
        error = composeError("Username cannot be empty", {
            type: "Validation",
            severity: "Low",
            metadata: { key: 'username', code: 'empty' }
        })
        return (null, )
    endif

    if isNullOrEmpty(password):
        error = composeError("Password cannot be empty", {
            type: "Validation",
            severity: "Low",
            metadata: { key: 'password', code: 'empty' }
        })
        return (null, error)
    endif

    return (true, null)
end function

function createUser(username, password):
    (isValid, error) = validateUserData(username, password)

    if error != null:
        # Return error with a message for the user
        userError = composeError("Could not create the user, please check the data and try again", {
            type: "Validation",
            severity: "Low",
            cause: error,
            metadata: error.metadata # Just pass the metadata to the user error, so the ui can use it
        })
        return (null, userError)
    endif

    # Because we are dealing with an external system,
    # we need to handle the possible unexpected errors using the built-in try-catch mechanism
    try
        user = Database.CreateUser(username, password)
        # If the user is created successfully, return it
        return (user, null)
    catch exception:
        # If we get an exception, convert it to an IEDD error to be properly handled
        # First create the error for the developers or monitoring system, this error will not be sended to the user
        databaseError = createError("Database error", {
            type: "Database",
            severity: "Critical",
            cause: exception,
            metadata: {
                query: "INSERT YOUR PREFERRED DB QUERY HERE",
             }
        })

        # We can perform corrective actions here or, ideally, up in the call chain

        # Return the error for the caller to handle it. This is the error that will be sended to the user
        return (null, createError("Could not create the user, please try again later", {
            type: "Database",
            severity: "Critical",
            cause: databaseError
        }))
    end try
end function

# Usage:
(user, error) = createUser("user", "pass")

if error != null:
    if error.type == "Validation":
        # Handle the validation error
        # Maybe sending a 422 HTTP response to the user
        # Because is an expected low severity error, we don't need to perform corrective actions
    else
        # Handle the database error or any other error
        # If we haven't performed corrective actions in the original function, we can do it here
        # And maybe send a 500 HTTP response to the user with the generic error message
else:
    # Send the user data to the user or continue with the next task in the workflow
Enter fullscreen mode Exit fullscreen mode

Implementing a Structured Error Object in Various Languages

C

In C#, we can define a custom exception class to represent the structured error object.

class StructuredError : Exception
{
    string Type { get; }
    string Severity { get; }
    object Metadata { get; }
    Exception Cause { get; }

    StructuredError(string message, string type, string severity, Exception cause = null, object metadata = null)
        : base(message)
    {
        Type = type;
        Severity = severity;
        Cause = cause;
        Metadata = metadata;
    }
}
Enter fullscreen mode Exit fullscreen mode

Javascript

In JavaScript, the Error class can be extended to create our StructuredError class.

class StructuredError extends Error {
  constructor(message, { type, severity, cause, metadata }) {
    super(message);
    this.type = type;
    this.severity = severity;
    this.cause = cause;
    this.metadata = metadata;
  }
}
Enter fullscreen mode Exit fullscreen mode

Python

In Python, we can subclass the base Exception class for our structured error.

class StructuredError(Exception):
    def __init__(self, message, type, severity, cause=None, metadata=None):
        super().__init__(message)
        self.type = type
        self.severity = severity
        self.cause = cause
        self.metadata = metadata
Enter fullscreen mode Exit fullscreen mode

Go

In Go, error handling is typically done using custom types that satisfy the error interface. Error handling in Go.

For this, we can create a StructuredError type for this purpose using the struct type.

type StructuredError struct {
    Message  string
    Type     string
    Severity string
    Cause    error
    Metadata interface{}
}

func (e StructuredError) Error() string {
    return e.Message
}

func NewStructuredError(message, errorType, severity string, cause error, metadata interface{}) *StructuredError {
    return &StructuredError{
        Message:  message,
        Type:     errorType,
        Severity: severity,
        Cause:    cause,
        Metadata: metadata,
    }
}
Enter fullscreen mode Exit fullscreen mode

This structured error object is a proposal stemming from the Integral Error-Data Duality (IEDD) paradigm. It offers a standardized approach to encapsulating and conveying error information. However, its adoption and precise implementation details are at the discretion of you and your team.

Regardless of the chosen implementation strategy, we must adhere to the core principles of IEDD. This ensures that errors are treated with equal importance as valid data and have the metadata needed to perform corrective actions if needed, promoting clarity, predictability, and resilience in software design and development.

IEDD and Go

Inspiration from Go

Go's standard practice of returning both results and errors from functions has been a significant influence on the IEDD paradigm. This common practice in Go has made developers inherently more aware of potential errors and has impulsed an environment where handling errors becomes a natural and inseparable part of regular programming. Instead of relegating errors to the background, this practice brings them to the forefront, making them an intrinsic part of every function's return signature.

This way of approaching error handling has, in many ways, acted as a precursor to the IEDD philosophy, where errors are elevated to the same level of importance as valid data. Such approach not only promotes better code practices but also ensures that we give due diligence to error scenarios, ultimately resulting in more robust and fail-safe applications.

Considerations for Full IEDD Adoption in Go

While Go's return pattern is in alignment with the IEDD's principles, there are two considerations to keep in mind for a full-fledged adoption:

  • Explicitness Over Convention: In Go, while it's common to return errors as the last value, it's more of a common practice than a strict rule. For IEDD to be effective, this pattern should be made more explicit, ensuring that every function that can return an error does so consistently.

  • Unified Representation: Go doesn't inherently enforce a uniform structure for returned data and errors. While in practice many functions return a result followed by an error, others in the same code base might return multiple results along with or without an error. Adopting IEDD would mean standardizing and enforcing this representation.

IEDD and Vladimir Khorikov's Result Object Pattern

Vladimir Khorikov's Result Object Pattern is a popular approach to error handling that, although not explicitly stated, is in alignment with the IEDD paradigm.

Result Object Pattern

The main goal of the Result Object Pattern was to handle operation results without throwing exceptions, especially for domain logic. This is because exceptions, especially in .Net, can be costly in terms of performance.

The Result's shape was thought of as a way to represent the result of an operation that can fail and encapsulates the final value of the operation if succeeded, or one or more errors if failed.

The resulting value usually (if succeeded) is one of the Domain Objects involved in the operation, like an Entity or a Value Object. But this is not a requirement, the result value can be any type of value.

Here is a simple example of the Result Object Pattern in pseudocode:

class Result<T>:
    bool isSuccess
    string? error
    T? value

    private Result(bool isSuccess, T? value, string? error):
        this.isSuccess = isSuccess
        this.error = error
        this.value = value
    end constructor

    static Ok<T>(T value):
        return new Result<T>(true, value, null)
    end static method

    static Fail<T>(string error):
        return new Result<T>(false, null, error)
    end static method

endclass

class User:
    string username
    string password

    private User(string username, string password):
        this.username = username
        this.password = password
    end constructor

    static Create(string username, string password):
        if isNullOrEmpty(username):
            return Result<User>.Fail("Username cannot be empty")
        endif

        if isNullOrEmpty(password):
            return Result<User>.Fail("Password cannot be empty")
        endif

        return Result<User>.Ok(new User(username, password))
    end static method
end class

# Usage:
result = User.Create("user", "pass")

if result.isSuccess:
    # Use result.value
else:
    # Handle result.error
Enter fullscreen mode Exit fullscreen mode

Using the Result Object Pattern with IEDD

Note: For simplicity, we will not use the structured error object in this example, but it can be used to encapsulate the error information as stated in the previous sections.

C

In C# we can use the same class that was used in the previous example.

class Result<T>
{
    bool IsSuccess { get; private set; }
    string? Error { get; private set; }
    T? Value { get; private set; }

    private Result(bool isSuccess, T? value, string? error)
    {
        IsSuccess = isSuccess;
        Error = error;
        Value = value;
    }

    static Result<T> Ok(T value)
    {
        return new Result<T>(true, value, null);
    }

    static Result<T> Fail(string error)
    {
        return new Result<T>(false, null, error);
    }
}

public Result<bool> ValidateUserData(string username, string password)
{
    if (string.IsNullOrEmpty(username))
        return Result<bool>.Fail("Username cannot be empty");

    if (string.IsNullOrEmpty(password))
        return Result<bool>.Fail("Password cannot be empty");

    return Result<bool>.Ok(true);
}

// Usage:
var result = ValidateUserData("user", "pass");
if (!result.IsSuccess)
{
    // Handle or propagate the error to the caller
}
else
{
    // Continue with the next task in the workflow
}
Enter fullscreen mode Exit fullscreen mode

Javascript

class Result {
  constructor(isSuccess, value, error) {
    this.isSuccess = isSuccess;
    this.value = value;
    this.error = error;
  }

  static Ok(value) {
    return new Result(true, value, null);
  }

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

function validateUserData(username, password) {
  if (!username) return Result.Fail("Username cannot be empty");

  if (!password) return Result.Fail("Password cannot be empty");

  return Result.Ok(true);
}

// Usage:
const result = validateUserData("user", "pass");
if (!result.isSuccess) {
  // Handle or propagate the error to the caller
} else {
  // Continue with the next task in the workflow
}
Enter fullscreen mode Exit fullscreen mode

Python

class Result:
    def __init__(self, isSuccess, value, error):
        self.isSuccess = isSuccess
        self.value = value
        self.error = error

    @staticmethod
    def Ok(value):
        return Result(True, value, None)

    @staticmethod
    def Fail(error):
        return Result(False, None, error)

def validate_user_data(username, password):
    if not username:
        return Result.Fail("Username cannot be empty")
    if not password:
        return Result.Fail("Password cannot be empty")
    return Result.Ok(True)

# Usage:
result = validate_user_data("user", "pass")
if not result.isSuccess:
    # Handle or propagate the error to the caller
else:
    # Continue with the next task in the workflow
Enter fullscreen mode Exit fullscreen mode

Go

In Go we don't have classes but we can use structs to implement the Result Object Pattern.

type Result struct {
    IsSuccess bool
    Value     interface{}
    Error     error
}

func Ok(value interface{}) Result {
    return Result{true, value, nil}
}

func Fail(err error) Result {
    return Result{false, nil, err}
}


func ValidateUserData(username, password string) Result {
    if username == "" {
        return Fail(errors.New("Username cannot be empty"))
    }
    if password == "" {
        return Fail(errors.New("Password cannot be empty"))
    }
    return Ok(true)
}

// Usage:
result := ValidateUserData("user", "pass")
if !result.IsSuccess {
    // Handle or propagate the error to the caller
} else {
    // Continue with the next task in the workflow
}
Enter fullscreen mode Exit fullscreen mode

Potential Challenges

Complexity for Simpler Use-Cases

While the IEDD paradigm strives for clarity and predictability, it might seem like an overkill for very basic applications or tasks. The additional layers introduced by the structured error objects or the dual return values can be seen as superfluous. However, this consistency can be immensely beneficial in the long run. It ensures that as the application grows, the error-handling mechanism remains robust and doesn't need an exhaustive refactor. For truly minimalistic applications, developers can opt for a more simplified version of IEDD, striking a balance between simplicity and clarity. Take into account that pure functions don't need to return errors.

Performance Overheads

The concern that IEDD might introduce performance costs, especially with the consistent return of structured error objects, is not entirely baseless. However, it is essential to weigh these potential overheads against the larger picture. Modern programming environments are highly optimized, and the marginal performance cost is often negligible compared to the benefits of maintainable and clear code. Moreover, in most real-world scenarios, the primary computational demands will eclipse the negligible costs of this error-handling approach. Also, take into account that in some cases, throwing and catching errors can be more costly than returning the data-error duality.

Learning Curve and Adoption in New Members

Introducing a new paradigm never comes without its challenges. We are accustomed to traditional error-handling techniques, focusing on business logic first and then catching any errors that may arise later. The adoption of IEDD principles may initially seem unnecessary or counterintuitive, and this may be more pronounced in new team members unfamiliar with the paradigm. However, the initial effort invested in mastering the IEDD principles will yield results in future development phases. As the paradigm implementation progresses, the development process will gain momentum and become smoother, simpler and more intuitive.

Integration with Existing Systems

Legacy systems, with their established error-handling mechanisms, might not immediately align with IEDD principles. But this doesn't necessitate a complete system refactor. A phased approach, where new components adopt IEDD and older parts are refactored over time, can make this transition manageable. This iterative integration ensures that systems benefit from improved error handling without significant disruptions.

Note: A useful strategy to not break the rule is not a problem until it is a problem is to apply the IEDD principles every time an existing part of the code needs to be modified or a new feature is added. This way, the IEDD principles are applied gradually and the codebase is improved over time.

Error Obscuration

There's a valid concern that with IEDD, errors might become obscured or diluted, given the dual nature of the return values. However, this paradigm's very essence is to give errors the prominence they deserve. By design, it discourages brushing errors under the carpet. Empowered with clear conventions, better introspection and metadata and a consistent return pattern, errors are given the attention they need to be handled effectively.

Loss of Native Error Handling Features

While IEDD offers a structured approach to handling errors, it doesn't advocate for the complete abandonment of native error-handling features. Instead, it complements them. We can and should continue leveraging built-in mechanisms for unexpected errors, with IEDD serving as a more graceful conduit for anticipated error scenarios. Also, as shown in the used examples, the native error or exception class is still used to encapsulate the error information, in this way the native error stack trace is preserved.

Standardization Challenges Across Teams

Ensuring a uniform implementation of IEDD across diverse teams or multiple projects might initially present challenges. Different teams might have their unique adaptations of the paradigm based on their specific contexts and needs. However, this diversity can be an asset. It encourages teams to exchange insights and best practices, leading to a more enriched and evolved understanding of the IEDD approach which adapts to the specific needs of each team.

Summary

The Integral Error-Data Duality (IEDD) paradigm represents a shift in how to perceive and handle errors within software systems. By treating errors with the same significance as valid data, IEDD ensures a more transparent, consistent, and resilient approach to software design and development.

At its core, IEDD emphasizes the coexistence of data and errors, ensuring that anticipated workflow errors and unexpected exceptions are addressed methodically. The introduction of structured error objects and a consistent return pattern offers clarity and predictability, making error-handling an integral part of the development process, rather than an afterthought.

While the paradigm brings with it numerous advantages, it's essential to approach its adoption with a clear understanding of its principles and potential challenges. But, as with any transformative approach, the benefits, in terms of maintainability, clarity, and robustness, are well worth the effort.

Embracing the IEDD paradigm promises not only to enhance the quality of software but also to elevate the developer experience. It invites a proactive stance towards errors, turning potential pitfalls into opportunities for growth and improvement. So, as you embark on your next software project, consider integrating IEDD principles. It's more than just a methodology; it's a mindset shift towards holistic and harmonious software design.

Top comments (0)