Odin»Blog

Exceptions - And Why Odin Will Never Have Them

Original Comments:
https://github.com/odin-lang/Odin/issues/256#issuecomment-418073701
https://github.com/odin-lang/Odin/issues/256#issuecomment-418289626


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.

---------

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:
1
2
3
4
5
6
7
8
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:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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
1
2
3
4
try:
	x = foo()
except Exception:
	pass # An error has happened

Odin:
1
2
3
4
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:
1
2
3
4
5
6
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:
1
2
3
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.

Mārtiņš Možeiko, Edited by Mārtiņš Možeiko on

If C++ exceptions are your thing, then I like this approach: https://channel9.msdn.com/Shows/G...cu-Systematic-Error-Handling-in-C

It allows callee to return "exception state" to caller. And let it deal with either handling exception with exception handler, or as a regular if statement check. Or not to deal with exception, but just throw real C++ exception if it does not want to handle exceptions and pass it to its own caller.
Neo Ar, Edited by Neo Ar on
I like the combination of defer and the `until` construct discussed in Donald Knuth's "Structured Programming with Goto Statements," which is similar in flavour (in some respects) to exceptions.

Defer needs no explanation, but the until thing essentially lets you write code like this (imaginary C-like syntax in foo_callsite, renaming Knuth's `until` to `try`):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
enum
{
    ValueError = 1,
    BarError,
    BazError,
    PlopError,
};
int foo(int * x)
{
    int a = 42;
    if (!x)
        return BarError;
    else if (*x < 0)
        return ValueError;
    else if (*x < a)
    {
        *x = a - *x;
        return BazError;
    }
    else if (*x > INT_MAX - a)
    {
        *x = a - (INT_MAX - *x);
        return PlopError;
    }
    if (*x < 1 << 16) *x -= a;
    else *x += a;
    return 0;
}
void foo_callsite(int * x)
{
    //...
    try ((VALUE_ERROR, int x),
         (BAR_ERROR, char * s),
         (BAZ_PLOP_ERROR, int x, int e))
    {
        //...
        int e = foo(x);
        if (!e)
        {
            //...
        }
        else if (e == ValueError)
            situation (VALUE_ERROR, *x);
        else if (e == BarError)
            situation (BAR_ERROR, "Ya dun goofed");
        else if (e == BazError || e == PlopError)
            situation (BAZ_PLOP_ERROR, *x, e);
        //...
    } situation (VALUE_ERROR, int x)
    {
        printf("VALUE ERROR: %d\n", x);
    } situation (BAR_ERROR, char * s)
    {
        printf("BAR ERROR: %s\n", s);
    } situation (BAZ_PLOP_ERROR, int x, int e)
    {
        printf("BAZ or PLOP? %s\nSlop: %d\n", e == BazError ? "BAZ" : "PLOP", x);
    }
    //...
    return;
}


... and it lowers to something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
enum
{
    ValueError = 1,
    BarError,
    BazError,
    PlopError,
};
typedef union
{
    struct
    {
        int x;
        int __;
    } value;
    struct
    {
        char * s;
    } bar;
    struct
    {
        int x;
        int e;
    } baz_plop;
} FooSituationArgs;
int foo(int * x)
{
    int a = 42;
    if (!x)
        return BarError;
    else if (*x < 0)
        return ValueError;
    else if (*x < a)
    {
        *x = a - *x;
        return BazError;
    }
    else if (*x > INT_MAX - a)
    {
        *x = a - (INT_MAX - *x);
        return PlopError;
    }
    if (*x < 1 << 16) *x -= a;
    else *x += a;
    return 0;
}
void foo_callsite(int * x)
{
    //...
    FooSituationArgs a;
    {
        //...
        int e = foo(x);
        if (!e)
        {
            //...
        }
        else if (e == ValueError)
        {
            a.value.x = *x;
            goto VALUE_ERROR;
        }
        else if (e == BarError)
        {
            a.bar.s = "Ya dun goofed";
            goto BAR_ERROR;
        }
        else if (e == BazError || e == PlopError)
        {
            a.baz_plop.e = e;
            a.baz_plop.x = *x;
            goto BAZ_PLOP_ERROR;
        }
        //...
    }
    goto END;
VALUE_ERROR:
    printf("VALUE ERROR: %d\n", a.value.x);
    goto END;
BAR_ERROR:
    printf("BAR ERROR: %s\n", a.bar.s);
    goto END;
BAZ_PLOP_ERROR:
    printf("BAZ or PLOP? %s\nSlop: %d\n", a.baz_plop.e == BazError ? "BAZ" : "PLOP", a.baz_plop.x);
END:
    //...
    return;
}

Simon Anciaux,
@miotatsu Can you elaborate on what is interesting with this ? To me it looks like a switch statement, except hard to write and read.
Neo Ar,
Of course :)

Since you asked about it in comparison to a switch, to start with here is the relevant portion of my example as a semantically equivalent switch:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void foo_callsite(int * x)
{
    //...
    FooSituationArgs a;
    {
        //...
        int e = foo(x);
        switch (e)
        {
            case 0:
                //...
                break;
            case ValueError:
                a.value.x = *x;
                goto VALUE_ERROR;
            case BarError:
                a.bar.s = "Ya dun goofed";
                goto BAR_ERROR;
            case BazError:
            case PlopError:
                a.baz_plop.e = e;
                a.baz_plop.x = *x;
                goto BAZ_PLOP_ERROR;
            case default:
                //...
                break;
        }
    }
    goto END;
VALUE_ERROR:
    printf("VALUE ERROR: %d\n", a.value.x);
    goto END;
BAR_ERROR:
    printf("BAR ERROR: %s\n", a.bar.s);
    goto END;
BAZ_PLOP_ERROR:
    printf("BAZ or PLOP? %s\nSlop: %d\n", a.baz_plop.e == BazError ? "BAZ" : "PLOP", a.baz_plop.x);
END:
    //...
    return;
}


In practice of course you would not have the union and the gotos and just handle the errors directly in the cases but it should be noted that the situations (errors in this example) are not handled within the same scope in the construct I am talking about, thus the variables being passed as arguments

The important difference is that this construct can do a lot more than a switch. A switch is a nice construct in that it can be optimised to a branch table or binary search, whereas this is simply lowering to gotos - it is not intended to replace a switch. Let's look at some of the other things we can do with situations.

A switch jumps to one of many locations depending on the value of a variable. A situation on the otherhand is an unconditional jump

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
//note: this is a new example, not an extension of the previous
try ((DouglasAdams), (JonBlow))
{
    int a = 42;
    if (a == 42) situation (DouglasAdams);
    char * b = "Hello, World!"
    if (!strcmp(b, "Hello, Sailor!") situation (JonBlow);
} situation (DouglasAdams)
{
    //...
} situation (JonBlow)
{
    //...
}


Another thing you can do with situations is have them as a looping construct (i.e. "loop until one of these situations occurs"). I don't remember/know the language Knuth was using (Pascal?) but he presented the construct in `begin until` and `loop until`. `begin until` is what I am calling `try` except that it was required to exit the block with one of the situations (which I think is needlessly restrictive).

Here is a loop example from Knuth translated to my example syntax:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
try until ((left_leaf_hit), (right_leaf_hit))
{
    if (A[i] < x)
    {
        if (L[i] != 0)
            i = L[i];
        else
            situation (left_leaf_hit);
    }
    else
    {
        if (R[i] != 0)
            i = R[i]
        else
            situation (right_leaf_hit);
    }
} situation (left_leaf_hit)
{
    L[i] = j;
} situation (right_leaf_hit)
{
    R[i] = j;
}
A[j] = x; L[j] = 0; R[j] = 0; ++j;


If you'd like to see more examples, I'd recommend checking out "Structured Programming with Goto Statements." I should also mention that Knuth didn't invent the construct, he credits it to C. T. Zahn, Peter Landin, Clint & Hoare, and some similar work by others yet

With regards to it being "hard to write and read," I don't agree, although I agree the syntax I made up here is not perfect. Ideally the parameter list should not need to be specified both in the situation indicator list within the () of the try/until and before the situation itself. I wrote it this way because I haven't thought at all about how it would be parsed and figured extra verbosity in examples are fine. I also don't like the keyword to do the jump being `situation` but I'm not sure what a better keyword would be for "this situation occured." Knuth's examples don't use a keyword but I don't think that is friendly to parsing

The syntax I wrote here is meant to mimic the syntax of exceptions, I don't see how this is harder to read than

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class VALUE_ERROR
{
    int x;
    VALUE_ERROR(int x) : x(x){} 
};
class BAR_ERROR
{
    const char * s;
    BAR_ERROR(const char * s) : s(s){}
};
class BAZ_PLOP_ERROR
{
    int x;
    int e;
    BAZ_PLOP_ERROR(int x, int e) : x(x), e(e){}
};
void foo_callsite(int * x)
{
    //...
    try
    {
        //...
        int e = foo(x);
        if (!e)
        {
            //...
        }
        else if (e == ValueError)
            throw VALUE_ERROR(*x);
        else if (e == BarError)
            throw BAR_ERROR("Ya dun goofed");
        else if (e == BazError || e == PlopError)
            throw BAZ_PLOP_ERROR(*x, e);
        //...
    } catch (VALUE_ERROR& e)
    {
        printf("VALUE ERROR: %d\n", e.x);
    } catch (BAR_ERROR& e)
    {
        printf("BAR ERROR: %s\n", e.s);
    } catch (BAZ_PLOP_ERROR& e)
    {
        printf("BAZ or PLOP? %s\nSlop: %d\n", e.e == BazError ? "BAZ" : "PLOP", e.x);
    }
    //...
    return;
}

(If that has errors it's because I haven't done C++ in ages and didn't try to compile it)
Simon Anciaux,
Thanks. I didn't get that you tried to mimic the "exception syntax".
In your C++ exception example I think you would throw the exception from the foo function and not have to do the ifs in the caller.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int foo( ){
    if ( ... )
       throw ValueError;
    else if ( ... )
       throw BazError;
    ...
}

void call( ){
    try {
        int x = foo( );
    }
    catch( ValueError& e ){ ... }
    catch( BazError& e ) { ... }
}
Neo Ar,
Yeah, of course situations are no replacement for propogating errors up the stack, same can be said about what was being discussed with Odin - if you want that you'd need to longjmp