Idiomatic way to print any error

I am trying to unlearn some of the golang’s idioms, namely interfaces.
In golang an error is anything that has Error() string method and this is very convenient - any error received from anywhere can be easily printed as a string.

There are no interfaces in Odin and errors are often represented as unions of structs.
I know there are several mechanisms in odin, like parametric polymorphism, explicit procedure overloading and finally explicit vtables (like with allocators).

I am struggling to put 2 and 2 together and understand how to print any error (mine or from a library)? Or is the very idea of printing an error is incorrect?

No, not really, but it make me question why you want to print every error…

Is it because all you want is just a single place to do error handling, and otherwise just bubble things up?

If so, that’s what you should unlearn…

— Handle errors where they happen, when they happen unless there’s a strong reason not to. —

If you handle errors locally, you have the context to log the necessary information, or wrap one error type into another to add the extra context for whomever consumes it in the end.
But generally, the moment you logged the error the error indication just becomes a bool…

In case you are writing a library, you could have a -> (result: T, ok: bool) and -> (result: T, err: Error) variant of fallible procs, where you put logging in the bool version (use core:log not core:fmt) otherwise leave it up to the user.


However, if you are looking for a way to define printers for error types to be used in logging, you are probably looking for fmt.register_user_formatter.

Example:

package err

import "base:runtime"
import "core:fmt"
import "core:io"
import "core:os"

Reason :: enum {
	Super_Green = 0,
	Big_Bada_Boom,
}

@(rodata)
REASON_STR := [Reason]string {
	.Super_Green   = "false alarm",
	.Big_Bada_Boom = "call Korben Dallas, he knows how to solve this",
}

Error_Context :: struct {
	loc:          runtime.Source_Code_Location,
	reason:       Reason,
	as_exit_code: int,
}

fails :: proc(x: int, loc := #caller_location) -> (y: int, err: Maybe(Error_Context)) {
	if x > 128 {
		err = Error_Context {
			loc          = loc,
			reason       = .Big_Bada_Boom,
			as_exit_code = 127,
		}
	} else {
		y = x + 1
	}
	return
}

main :: proc() {
	if _, maybe_err := fails(129); maybe_err != nil {
		err := maybe_err.?
		fmt.eprintfln("%s", err)
		os.exit(err.as_exit_code)
	}
	unreachable()
}

@(init)
init_formatters :: proc() {
	@(static) FMTS: map[typeid]fmt.User_Formatter

	FMTS = make(type_of(FMTS))
	fmt.set_user_formatters(&FMTS)
	if fmt.register_user_formatter(Error_Context, fmt_error_context) != nil {
		panic("didn't even had a chance")
	}
}

@(private)
fmt_error_context :: proc(info: ^fmt.Info, arg: any, verb: rune) -> bool {
	if err, ok := arg.(Error_Context); ok {
		switch verb {
			case 's':
				fmt.fmt_value(info, cast(any)err.loc, 's')
				io.write_string(info.writer, " error: ", &info.n)
				io.write_string(info.writer, REASON_STR[err.reason], &info.n)
				io.write_string(info.writer, " (code: ", &info.n)
				io.write_int(info.writer, cast(int)err.as_exit_code, 10, &info.n)
				io.write_byte(info.writer, ')', &info.n)
				return true
			case:
				fmt.fmt_bad_verb(info, verb)
		}
	}
	return false
}
2 Likes

I had to re read this several times before I stopped shaking from laughing. I just rewatched 5th Element the other day🤣…

1 Like