(maybe) Better/Nicer Error handling


#1

rust could need a better error handling system - very partial exception like - but different in handling

i don’t like the Result system for returning error because: -its an in-between of a simple c-like error-code, std::optional + user additional information -its still is possible to forget the handling of the result (like an error-code) -your be able to unwrap non-Err -Result does not follow zero-cost-abstraction rule -the Result is a big as the Union of all Result-Variants-Informations -Result teaches programmers to using simple errors or helps in building fat unions

for example C++ Exceptions does not suffer from these negatives - but heap allocation is done on throw and Exceptions do not stop on functions scope (shoot back through the stack) thats sometime not very nice

the idea could be just bullshit out of the box - but maybe its helpfull in any way

Example:

fn maybe_error(error: bool) -> Result<int, int> {
 if error {
   Err(1)
 } else {
   Ok(2)
 }
}
fn main() {
  {
    //normal usage
    match maybe_error(true) {
      Ok(a) => println!("Ok {}", a),
      Err(b) => println!("Err {}", b)
    }
  
    //maybe_error(true).unwrap(); // FAIL - only a runtime error - not compiltime-fail (due to the Result nature)
  }
  
  {
    maybe_error(false).unwrap(); // "not" handled
  }
}

the idea is to use some freaky sort of inline closure for error handling given to the Error “throwing” function by the compiler

fn maybe_error(error: bool /*hidden Error closure Stack Parameter*/ my_error, my_special_error, ... as many specialized Errors thrown, but not all everytime used)
 if error {
   fail my_error(1); --> fail calls hidden closure my_error
   //-->return from maybe_error after error handled in main
 } else {
   //ok
 }
 fail my_special_error(data_on_heap_300kb, on_stack_200byte, on_stack_100byte); --> fail calls hidden Lambda my_special_error
 //-->return from maybe_error after error handled in main
}

-----> the compiler constructs an hidden closure for every "catch" and attache them  to the "throwing" function by "hidden" parameters (just on stack)
-----> the closure is called in context (no need to copy or heap the error-info - just stack) of maybe_error with main-scope access - as normal when the closure is defined in main-scope
 
fn main() {
  int my_value = 10;
  try maybe_error(true /*hidden error handling closures: &my_error, &my_special_error*/...) {
    my_error ==> { forms the hidden closure
    	print("nr {0} {1}", my_error.nr, my_value);
    }
    special_error => { forms the hidden closure
      print("nr {0} {1} {2} {3}", special_error.data, special_error.another_info, special_error.blub, my_value)
    }
  }
}

PROs -this way there is no need to create big errors that can handle every error-information the function can return -existence of error handling can be checked at compiletime (not the “qualitiy” of the handling itself) -cleaner interfaces

CONs -language change, “fail/try” or other special syntax for returning error and the try/match/closure behavior -functions needs to express to the compiler what exception types can be “thrown” -try/catch code-generation needs more analyse before -linking - i don’t know how that works in Rust - could be maybe problematik…

simplified-version / different signature idea

fn maybe_error(int value) -> int ==> fails blub(int x, int y),blib(str),blab(float,str,int) <-- becomes part of the signature (some sort of named closures)
{
  if 1 ... fail blub(10,203.34);

  return 20;

  if 2 ... fail blib("hello");

  if 3 ... fail blab(223.0, "hello",1+1);

  return 10;
}

fn maybe_error2() 
{
  try
    maybe_error
  failed
    blub(int, float) => { closure runs in context of mabye_error }
    blib(str) => { closure runs in context of mabye_error }
    blab(float, str, int) => { closure runs in context of mabye_error }
}

//functions with simple Result are still simple

fn maybe_error3() -> int fails (int)
{
  if(xyz) fail(10);
  return 20;
}

pros -feels like an clean symbios between the Result/match and “Exceptions” -no heap allocation -no copy (still in failing function context) -inline able if not in external lib -immutablity reserved cleanly -no fat Result-Types -no misuse of Result -clean error distinction -the error becomes part of the signature (happens also to the Result-Type - if not using int for everything) -keeps error local (like Result) - no “exception” like error spreading

cons -changes needed -(if that is a problem) does not behave like java,c#,c+±exceptions


#2

Have you looked at https://github.com/rust-lang/rfcs/pull/243?


#3

@Jexell - as i understand it correct it bases on Result

so is my negative point

“the Result is a big as the Union of all Result-Variants-Informations”

is still negative with this RFC - or?


#4

I don’t know exactly how enums are implemented, but I think your point may not be an issue anymore as all errors can be turned into a Box<Error>.


#5

i think enums are union-like
and Box means heap allocation - something i want to prevent


#6

If maybe_error is defined in a third-party library, we will have a difficulty in supplying appropriate error-hander closures and will want to ask the author to define an enum to clarify the nature of errors…


#7

i don’t know how Rust handles linking with third-party, your’re right if its nearly identical to C (you can link if you know the interface) but i thought rust got/needs more information for linking - and the hidden closure-paramerts could be expressed by function meta-data (comming from the lib)


#8

@nodakai

…rethought…

the Result-Type sufferes from the same problem if you add additional information to the Result-Type and change code in maybe_error giving out these additional infos - only preventable when using boring int-values as error-codes