New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error handling discussion #256

Open
Alihusejn opened this Issue Sep 2, 2018 · 15 comments

Comments

Projects
None yet
10 participants
@Alihusejn
Copy link

Alihusejn commented Sep 2, 2018

Please do not turn Odin into second Go. Add Try/Except(or Catch). I think that Odin will become more popular than now.

@kiljacken

This comment has been minimized.

Copy link

kiljacken commented Sep 2, 2018

How does not having exceptions make Odin into Go?

While I do agree that the Go way of handling errors is less than optimal, as it tends to produce a lot of duplicated error handling code, I beleive it is far superior to exceptions. My ideal error handling is the use of a "Result" type representing either the value of a computation if succesful, or an error message/object if not succesful (like the method commonly used in Rust). I've been out of the loop on Odin for a bit, so I am not sure if Odin has the facilities for such a type.

@ThisDrunkDane

This comment has been minimized.

Copy link
Member

ThisDrunkDane commented Sep 2, 2018

Run-time exceptions are a higher-level construct that does not fit into the vision of Odin. Also, adding a feature just because it would make Odin more popular is a terrible reason. If we just added for popularity we would also for example have GC.
But Odin is a low-level pragmatic language, so features have to been seen to add enough value for it to make it in. Adding exceptions added a huge overhead not only to the runtime, but also mentally as you can never be too sure of what exceptions any code will cast, where as return codes you can always see as the function explicitly returns it.
Plus, being a second (but better) Go isn't the end of the world, Bill has drawn a lot of inspiration from Go and I personally really like Go for what it was made for, Web Servers.

@ThisDrunkDane ThisDrunkDane changed the title Exceptions. Should we add exceptions? Sep 2, 2018

@ThisDrunkDane

This comment has been minimized.

Copy link
Member

ThisDrunkDane commented Sep 2, 2018

@kiljacken There isn't anything in Odin defining the error handling, that's still up to the individual programmer.

@Joshua-Ashton

This comment has been minimized.

Copy link
Contributor

Joshua-Ashton commented Sep 2, 2018

God no.

@bullno1

This comment has been minimized.

Copy link

bullno1 commented Sep 3, 2018

My suggestion is that if there is something builtin for error handling, it should just be purely syntactic sugar.

In C, usually I have a macro called SOMETHING_CHECK e.g: GFX_CHECK and call it like: SOMETHING_CHECK(call_that_may_fail(...)) and it works fine albeit, a bit verbose but still better than the Go way of copy and paste that if statement.

A macro/metaprogramming system or even a specific error handler system could alleviate the problem. For example (made up syntax):

with my_error_handler {
   a := call_that_can_fail();
   b := another_call(a);
}

And it will be rewritten into:

my_error_handler(a, call_that_can_fail());
my_error_handler(b, another_call(a));

my_error_handler can then be a macro that check the return value, decides to whether return early or continue with execution.

Some type guard/check might be needed so that you can use two handlers in the same with statement for example.

This is basically the idea of Haskell's monad but modified for an imperative language. This is explicit (with block) and incurs no extra cost if you already do that anyway manually. Nothing about the runtime or function signature will change.

@luciusmagn

This comment has been minimized.

Copy link

luciusmagn commented Sep 3, 2018

@ThisDrunkDane

This comment has been minimized.

Copy link
Member

ThisDrunkDane commented Sep 3, 2018

@bullno1 Odin has the option for multiple return values so how would that system know which of the parameters to use for it's checking? + some function might use different values for error codes, ints, enums, bools etc..

@gingerBill

This comment has been minimized.

Copy link
Member

gingerBill commented Sep 3, 2018

There will never be software exceptions in the traditional sense. I hate the entire philosophy behind the concept.

Go does have exceptions with the defer, panic, recover approach. They are weird on purpose. Odin could have something similar for exceptional cases.

You can the exact same semantics as a try except block by using a switch in statement. The same is true in Go. The difference is that the stack does not need to be unwinded and it's structural control flow.

Odin has discriminated unions, enums, bit sets, distinct type definitions, any type, and more. Odin also have multiple return values. Use the type system to your advantage.

I do hate how most languages handle "errors". Treat errors like any other piece of code. Handle errors there and then and don't pass them up the stack. You make your mess; you clean it.

@gingerBill gingerBill closed this Sep 3, 2018

@ThisDrunkDane ThisDrunkDane reopened this Sep 3, 2018

@ThisDrunkDane ThisDrunkDane changed the title Should we add exceptions? Error handling discussion Sep 3, 2018

@bpunsky

This comment has been minimized.

Copy link
Contributor

bpunsky commented Sep 3, 2018

I get the feeling that most people in this discussion have a fairly surface-level knowledge of Odin's syntax. This isn't an insult, I'm just saying there is far less of a problem than some of you seem to think. Odin has multiple returns as well as a declaration & condition variant of if, meaning that it's a trivial matter to return a bool to indicate whether a procedure call was successful or not along with the payload return, as well as returning custom error types.

if bytes, ok := os.read_entire_file("filename.ext"); ok {
    fmt.println(string(bytes));
}
else {
    // handle failure
}

// you can also ignore returns

bytes, _ := os.read_entire_file("filename.ext"); // `_` is a value-sink; anything written to it vanishes, it cannot be read from

You can also use types like this:

Error :: struct {
     message: string,
     code: int,
}

or this:

Error_Code :: enum {
    FILE_NOT_FOUND,
    OUT_OF_MEMORY,
    COMPUTER_HATES_YOU,
}

as return values.

This is a powerful approach and gives the programmer total control of how to handle errors, as well as making the potential for an error to occur completely explicit in a procedure's prototype. Anything more than this - involving error handlers, runtime exceptions and/or try/catch - serves to take back control from the programmer and make the presence of errors a complete mystery. It makes code less readable and, in fact, more error-prone. The only "positive," if you can call it that, is that it "hides" error-handling and allows the programmer to program as though errors don't exist, passing on responsibility from libraries or worse, crashing a user's program unnecessarily. This is just laziness, as far as I can see (and you can be lazy explicitly with _ 😛).

There are numerous ways to "manufacture" your own error-handling system in Odin - Bill mentioned the tagged unions, which can be nil, and a number of other language features. There's no good reason to add something like exceptions to the base language just to save a little bit of typing and a little bit of brainpower at the expense of robust code, when you can literally accomplish the exact same effect with return types or global/thread-local variables. Getting familiar with Odin reveals a lot about why it does or doesn't have certain features, and how sometimes the lack of a feature actually is a feature in the sense that it solves the underlying problem in a better or more elegant way.

@gingerBill

This comment has been minimized.

Copy link
Member

gingerBill commented Sep 4, 2018

To expand on what I mean by this statement:

You can the exact same semantics as a try except block by using a switch in statement.

Python:

try:
	x = foo()
except ValueError as e:
	pass # Handle error
except BarError as e:
	pass # Handle error
except (BazError, PlopError) as e:
	pass # Handle errors

Odin:

Error :: union {
	ValueError,
	BarError,
	BazError,
	PlopError,
}

foo :: proc() -> (Value_Type, Error) { ... }

x, err := foo();
switch e in err {
case ValueError:
	// Handle error
case BarError:
	// Handle error
case BazError, PlopError:
	// Handle errors
}

The semantics are very similar in this case however the control flow is completely different. In the exceptions case (shown with Python), you enclose a block of code and catch any exceptions that have been raised. In the return value case (shown with Odin), you test the return value explicitly from the call.
Exceptions require unwinding the stack; this is much slower when an exception happens compared to the fixed small cost of a return value.

In both cases, a "catch all" is possible:
Python

try:
	x = foo()
except Exception:
	pass # An error has happened

Odin:

x, err := foo();
if err != nil {
	// An error has happened
}

One "advantage" many people like with exceptions is the ability to catch any error from a block of code:

try:
	x = foo()
	y = bar(x)
	z = baz(y)
except SomeError as e:
	pass

I personally see this as a huge vice, rather than a virtue. From reading the code, you cannot know where the error comes from. Return values are explicit about this and you know exactly what and where has caused the error.

One of the consequences of exceptions is that errors can be raised anywhere and caught anywhere. This means that the culture of pass the error up the stack for "someone else" to handle. I hate this culture and I do not want to encourage it at the language level. Handle errors there and then and don't pass them up the stack. You make your mess; you clean it.

Go's built-in error type has the exact same tendency of people return errors up the stack:

if err != nil {
	return nil, err
}

From what I have read, most people's complaints about the Go error handling system is the if err != nil, and not the return nil, err aspect. Another complain people have is that this idiom is repeated a lot, that the Go team think it is necessary to add a construct to the language reduce typing in the draft Go 2 proposal.


I hope this has cleared up a lot of the questions regarding Odin's take on error handling. I think error handling ought to be treated like any other piece of code.

With many rules, there will be unexpected emergent behaviour.

P.S. If you really want "exceptions", you can longjmp until the cows come home.

@hasenj

This comment has been minimized.

Copy link

hasenj commented Sep 14, 2018

Reduce the surface area of where errors can occur inside your code.

For example, if you need to open a file, and you're not sure it exists, and want to handle failure, don't structure your code such that this function is 10 levels down a call stack.

Define types that can embed "errors" and make your code work with these types.

File_Content :: struct {
    filename: string;
    content: string;
    error: IO_Error;
}

read_file :: (path: string) -> File_Content {
    .....
}

One "advantage" many people like with exceptions is the ability to catch any error from a block of code:

I don't think that's what people like about exceptions. They like about it that they can ignore error cases while writing code; for example, they can write something like this:

read_file :: (path: string) -> string {
    .....
}

if the file doesn't exist, none of your code has to worry about that! You pretend everything succeeds in most of your code, and then just dedicate a small portion of the most top level function to handle "exceptions".

In my experience this creates sloppy programs; hides errors, makes them hard to debug. It's also why a lot of error messages in a lot of program are confusing: it's because the programmer himself cannot know what the error is at the exception catch site; they best they can do is say "looks like something went wrong".

@gingerBill

This comment has been minimized.

Copy link
Member

gingerBill commented Sep 14, 2018

I don't think that's what people like about exceptions. They like about it that they can ignore error cases while writing code...

That is a completely separate aspect. That is still possible with a switch statement and using the default case.

@hasenj

This comment has been minimized.

Copy link

hasenj commented Sep 15, 2018

That is still possible with a switch statement and using the default case.

That's still explicitly checking for an error.

I mean not even checking if an error has occurred.

Say you have procedure A that calls B that calls C that calls D that calls E that calls F.

F can 'throw' an exception, but none of the other procedures in the call chain have to check for an error at all. E can be written with only the happy case in mind.

def F(...):
       # does something that can throw an exception

def E(..):
     e1(...)
     e2(...)
     e3 = F(....)  # an exception could be thrown here but we pretend it never occurs
     e4(e3) # pretend e3 is all good

def D(...):
    d1(...)
    d2 = E(....)
    d3(...)
    d4(d2)

Only A has to check if an exception was thrown from B. All the other procedures, from B all the way to E don't have to contain any error checking code.

That's what people like about exception throwing.

It's also why people complain about this in Go:

if err != nil {
    return nil, err
}

Because in Python and other languages this step is automatic, but in Go and languages without exception you have to explicitly emulate exception throwing for every call using the if err != nil snippet.

@gingerBill

This comment has been minimized.

Copy link
Member

gingerBill commented Sep 15, 2018

That's still explicitly checking for an error.

I dislike error propagation which is implicitly passing the error up the stack. People like being lazy, so don't give them more opportunity to be so.

It's also why people complain about this in Go

And that's why I like Go, because error handling is explicit. The issue I have is that people just return the error and pass it up the stack again.

It doesn't matter how the errors are implemented (values or exception) in the case, it's the error propagation.

@rdebath

This comment has been minimized.

Copy link

rdebath commented Jan 1, 2019

P.S. If you really want "exceptions", you can longjmp until the cows come home.

Does longjmp break defer?
Basically, a working setjmp/longjmp would be the only thing I'd use exceptions for. So that the error (and hopefully code location and full stack) can be saved and a generic recovery run without leaking memory or "resources".

In detail, any open database transactions are rolled back, the error info written out to a log (or saved in the database even) perhaps some global variables cleared, maybe a 'rollback_session' stored procedure gets called, sleep for a few seconds and restart. Preferably without restarting the whole process and losing details (like network connections to a client and which session needs to be rolled back!)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment