Error returns with detail you might want to ignoire

I was wondering what is the “best” was in odin to handle this error return case. I’ve not managed to find any discussion of exactly this.

Pretend I have a function “render_init()” that initialised a render system for a game. All my code really needs to know is “did this work” to determine if it can continue to run the application. So returning a bool “ok” is probably fine.

For debugging though it is useful to return more than that, perhaps a string that the higher level code can either log, or display in an error panel or something.

In standard C++ you might use an exception. If an exception is thrown of any kind, the operation failed. If you want to look into the exception to get a message you can.

My thoughts are to return both a bool and a string, but then it’s more ugly to handle in the caller if you only care about the bool, or perhaps I could just return a string and if it’s empty there is no error. But that seems ugly.

I did look maybe to return this :-

Err_Type :: union {
string
}

Then could return a string or a nil if there was no error . it easy for code to check for nil if you don’t care what the error was but the text is there if you want it. But then allocating and disposing of the string becomes a bit painful.

Is there any good way to do this? references to other discussions I’ve not managed to find would be welcome!

1 Like

I’ve recently started doing something really horrible that probably no one should do which is have an Error enum with very long highly specific values, sort of like what you might put in a log statement.

Like, Font_Atlas_Texture_Missing_So_Cant_Init_Renderer instead of File_Not_Found.

You’ll end up with lots of enum values and I’m not saying I endorse it. It’s just something I’ve been trying lately instead of using log statements.

3 Likes

I’d not return strings, just enough info in the error type to format it into a proper error message.

  • descriptive enum names
  • source code location
  • any data that can be copied and not heap allocated

You can always add a custom formatter and just print the error, no reason to format a string… Also the closer you log the error to the origin the more details you have and the less error payload you need to return.

2 Likes

Thank you. I’ll probably just go with extended enum codes so I have enough to display to give someone a clue what didn’t work.

One option I thought about to is to “log” the exact reason if debug logging is enabled, and just return an error code.

Also I did some reading and @gingerBill seems to be of the opinion that error handling isn’t really a special case, it’s just one of the possible return values from a function.

If I think of my function (in my head) as being not called init_renderer but instead “init_renderer_and_return_information_on_what _happened” then error returns just kind of drop out more naturally perhaps as just being a state the program can be in.

Maybe I’m misunderstanding his thoughts entirely in which case I apologise, but thinking of it like this seems helpful

I don’t want to speak for Gingerbill but I think his comments should be taken in context with the broader conversation about errors in language design.

Every modern language seems to have a special error type but Odin doesn’t, and I’m guessing those comments are a defense of that choice.

That doesn’t mean error states don’t exist, just that you don’t need special features in the language to handle it. At least, that’s how I see it.

1 Like

So in this case, you want to add a payload to the error state. I’d argue you might want a “side-channel” along with the an error code. So you don’t pass the -> Maybe(string) around but rather have -> Error_Code and then query for the error string.

Even in C that is common BUT they usually rely on global state which is not a good idea.

Thank you. Wondering where you might store the error string if not in global state?
I guess this is now just general programming rather than Odin specific though so there are any number of possibilities.

I find find Odin in general faster to write than the “modern” C++ I need to write for my “day job” but when you get very used to one way of doing things it can be surprisingly hard to think how best to do things in a different language.

You store it in the “system” of the thing you are working with. So it’s not “global” as in a global variable, but “global” to that system.

How about you do something like this for now:

iferr :: proc(cond: bool, str: string, args: ..any, loc := #caller_location) -> bool {
	if cond do return true
	log.errorf(str, args, location = loc)
	return false
}

_type :: proc(x: $T) -> typeid {
	return typeid_of(type_of(x))
}

// Usage
array_get_ptr :: proc(a: ^Array($T), id: u16) -> (ptr: ^T, ok: bool) #optional_ok {
	iferr(id < a.count, "%v: Id %d out of bounds (count = %d)", _type(a), id, a.count) or_return
	return &a.items[id], true
}

If you want to handle the error string in some fancy way later instead of just logging it, you could change the implementation of iferr() when you actually need it.

2 Likes

Edit: I changed my below example to remove the global variable. It’s existence made my original question moot. With this change, I’m still curious how this approach would be characterized. Always looking to learn something :slight_smile:

To avoid passing strings around, and have a simple way to lookup a more defined statement of the error, I would do the following for a simple system.

To Bill’s comment, would this be considerred as “global” or “global to the system” or neither?

ErrorCode :: enum {
  OK,
  INVALIDPATH,
  READERROR,
  RANDOMFOO,
}

getErrorDescription :: proc(e: ErrorCode) -> string {
  description := [ErrorCode]string {
  .OK          = "",
  .INVALIDPATH = "Could not open file at location provided.",
  .READERROR   = "File openned successfully, but could not be read",
  .RANDOMFOO   = "Some random shenanigans occurred"
  }
  return description[e]
}

SomeProcedure :: proc(input: u32) -> (bool, ErrorCode) {
  //do some stuff and return error code if an error occured
  switch input {
  case 0: return true, .OK
  case 1: return false, .INVALIDPATH
  case 2: return false, .READERROR
  case 3: return false, .RANDOMFOO
  }
  return true, .OK
}

main :: proc() {
  //random test of function
  ok, err := SomeProcedure(rand.uint32() % len(ErrorCode))
  if ok {
    //go on and do stuff
    fmt.println("no error occured")
  }
  else {
    //handle error, print or log
    if ODIN_DEBUG { fmt.printfln("error: %s", getErrorDescription(err)) }
  }
}

Then if I want to see the error information I run it with the debug flag:

odin run . -debug
or
odin build . -debug

…or if some reason you wanted to return more than one error code, could use a bit_set instead:

ErrorCodeSet :: bit_set[ErrorCode]

SomeProcedure :: proc(input: [4]u32) -> (isok: bool, errors: ErrorCodeSet) {
  //do some stuff and return error code if an error occured
  for i in input {
    switch i {
    case 0: isok = true //everythinng is good, continue
    case 1: isok = false; errors += {.INVALIDPATH} //add error to list
    case 2: isok = false; errors += {.READERROR} //add error to list
    case 3: isok = false; errors += {.RANDOMFOO} //add error to list
    }
  }
  return
}

main :: proc() {
  //test function with all possible cases
  input: [4]u32 = {0,1,2,3}
  ok, errs := SomeProcedure(input)
  if ok {
    //go on and do stuff
    fmt.println("no error occured")
  }
  else {
    //handle errors, print or log
    if ODIN_DEBUG {
      for err in errs {fmt.printfln("error: %s", getErrorDescription(err))}
    }
  }
}
1 Like