6

I started writing C again after professionally doing Scala and Python for quite some time. After using Scala's "Option" and "Either" error handling patterns and recently tasted the Rust way I wanted something similar in C. So I came up with something like this:

typedef struct {
    int age;
    char* name;
} User;

typedef struct {
    char* error;
    User* value;
} UserResult;

UserResult get_user() {
    /* Some complicated user fetching process .. that fails */
    return (UserResult) { .error = "403 Unauthorized\n" };
}

int main(void) {
    UserResult res = get_user();
    if (res.error)
        handle_error(res.error);
    if (res.value)
        do_something(res.value);
    /* ... */
    return 0;
}

But this isn't really safe (we could access invalid pointers). How can I get something similar than the Scala or Rust way of handling errors in C ?

EDIT: corrected UserResult field name "value"

lemour_a
  • 155
  • 1
  • 7

2 Answers2

6

There are many ways to handle errors in existing C code in the wild. So many that it represents a non neglectable mental load to understand how to handle errors for various libraries, even more so when using several at the same time in a project.

Introducing a new way of handling errors could be beneficial, but on the flip side, it could be akin to this famous XKCD comic.

The Rust (and Scala, etc) way is indeed very powerful and has a few advantages over other older idioms.

However, the Result idiom is based on Sum Types, which are not natively supported by the C language. There is no guarantee that your UserResult is in a coherent state. user and error could be both null, or non-null.

This could be modified a bit, for example by using a union instead:

typedef struct{
    union {
        char* error;
        User user;
    }
    bool has_error;
} UserResult;

The has_error flag indicates which field is actually filled:

int main(void) {
    UserResult res = get_user();
    if (res.has_error)
        handle_error(res.error);
    else
        do_something(res.value);
    /* ... */
    return 0;
}

This is not perfect, as the C language does not enforce proper union access, instead invoking undefined behaviour when used incorrectly.

SirDarius
  • 41,440
  • 8
  • 86
  • 100
4

The problem is even with UserResult model in C the onus is still on the programmer to remember to do null-checks, that is, compiler is not enforcing it, so even though we declared UserResult nothing prevents us to mistakenly write

int main(void) {
  UserResult res = get_user();
  do_something(res.value); // Runtime error: oops I forgot to null-check and unfortunately compiler is still happy
  return 0;
}

whilst with Scala's Option programmer is forced by the compiler to deal with the situation, for example,

case class User(age: Int, name: String)
case class UserResult(value: Option[User], error: Option[String])

def get_user(): UserResult = {
  /* Some complicated user fetching process .. that fails */
  UserResult(value = None, error = Some("403 Unauthorized"))
}

def do_something(user: User) = ???

object Main extends App {
  val res = get_user()
  do_something(res.value) // Compile-time error: oops I forgot to "null-check" but fortunately the compiler is unhappy
}
Mario Galic
  • 47,285
  • 6
  • 56
  • 98