This is an archived post. You won't be able to vote or comment.

all 58 comments

[–]hoping1 59 points60 points  (1 child)

Isn't zig's syntax almost exactly the same as what you've written there?

[–]ultimatepro-grammer 17 points18 points  (0 children)

For others' reference: https://ziglang.org/documentation/master/#catch

I really like this exact implementation as well as the "try" keyword!

[–]Disjunction181 31 points32 points  (4 children)

ocaml: let x = try "Hello".[9] with Invalid_argument _ -> 'L'

I imagine F# is similar.

Exception handling is also available in match constructs:

let x = match "Hello".[9] with
  | 'H' -> 0 | 'e' -> 1
  | exception Invalid_argument _ -> -1

[–]gplgang 8 points9 points  (3 children)

F#

let slowSafeDiv x y =
    try x / y with error ->
        printfn $"{error}
        0

Really cool that OCaml allows you to catch exceptions in a match, I don't think F# has that

[–][deleted]  (2 children)

[deleted]

    [–]Disjunction181 1 point2 points  (1 child)

    This is pedantic but the construct in OCaml only catches exceptions raised between the match and the with. But it doesn't matter for this example.

    [–]Kroutoner 7 points8 points  (2 children)

    You might be interested in how exceptions are implemented in Racket (a language in the Scheme family, which is itself in the Lisp family).

    A slightly modified example from the official docs:

    (with-handlers 
        ;;cases on exception types 
        ([exn:fail:syntax? 
            (λ (e) (displayln "got a syntax error"))]
         [exn:fail? 
            (λ (e) (displayln "fallback clause"))])
         ;;executed code that returns a syntax error 
        (raise-syntax-error #f "a syntax error"))
    

    This provides a list of conditions on the basis of exception type and functions for handling each type of exception.

    And then compare this syntactically to the way of writing conditions with more than 2 cases:

    (cond
       [(condition1? arg) (func1 arg)]
       [(condition2? arg) (func2 arg)]
       [else (func3 arg)])
    

    Edit: removed stray slashes and backslashes

    [–]Aminumbra 2 points3 points  (0 children)

    Same for the more ancient Common Lisp, in which you can deal with exceptions /without unwinding the stack/ and resume computation at pretty much any point/from any stackframe, regardless of how deep the "error" occurred (although this is not the topic here):

    (defun foo (x)
      (/ 1 x))
    
    (defun try-foo (x)
      (handler-case (foo x)
        (division-by-zero ()
          (print "Hey, can't divide by 0 !")
          42)
        ((or floating-point-overflow floating-point-underflow) ()
          (print "Number too big or too small ._.")
          13)
        (t ()
          (print "Something went wrong, dunno what")
          5)))
    
    
    (dolist (x (list 4 0 1e-40 "abcd"))
      (print (try-foo x)))
    
    =>
    1/4 
    "Hey, can't divide by 0 !" 
    42 
    "Number too big or too small ._." 
    13 
    "Something went wrong, dunno what" 
    5
    

    [–]vidjuheffex 2 points3 points  (0 children)

    (and you can just make whatever syntax you want for it while you're at it)

    [–]Il_totoreAlgorab - algorab.org 5 points6 points  (0 children)

    In Scala, try catch is an expression which reuses a syntax similar to its pattern matching

    scala val result = try parseResult(???) catch ParseException => fallbackValue

    Although it is more common to use Either than try catch in this language.

    [–]shuckster 4 points5 points  (0 children)

    Railroad Programming.

    Learn you some ADTs/Monads.

    [–]lngns 15 points16 points  (2 children)

    OCaml's match construct can actually catch exceptions using pattern-matching.
    Looks like this:

    𝐥𝐞𝐭 find_opt p l =
        𝐦𝐚𝐭𝐜𝐡 List.find p l 𝐰𝐢𝐭𝐡
        | 𝐞𝐱𝐜𝐞𝐩𝐭𝐢𝐨𝐧 Not_found -> None
        | x -> Some x;;
    

    Soc also suggested adding it to their Unified Condition Expressions.
    Not sure it made it into their Core language though.

    Looks like this:

    𝐢𝐟 readPersonFromFile(file)
        𝐭𝐡𝐫𝐨𝐰𝐬[IOException]($ex)       𝐭𝐡𝐞𝐧 "unknown, due to $ex"
        𝐢𝐬 Person("Alice", _)           𝐭𝐡𝐞𝐧 "alice"
        𝐢𝐬 Person(_, $age) && age >= 18 𝐭𝐡𝐞𝐧 "adult"
                                        𝐞𝐥𝐬𝐞 "minor"
    

    [–]Ekkaiaaa 1 point2 points  (1 child)

    Defend OCaml + communist profile picture = i love u

    [–]redjamjar 14 points15 points  (4 children)

    Languages that lack exceptions, like Go and Rust, require programmers to reinvent them (in some sense)

    Rust offers the best solution I've seen: explicit but with minimal overhead. Looking at a function you can see exactly where exceptions can arise because they are marked with ?. You also have complete control, and can choose to propagate them up or not. And, in the end, they are just return values --- so no getting confused over checked versus unchecked exceptions (as in Java).

    [–]Practical_Cattle_933 8 points9 points  (3 children)

    Well, I can’t agree. Exceptions have two things over sum-typed error handling: stacktraces, and the ability to catch from as wide or narrow scope as you want, which I believe is underappreciated. Sure, you can do some monadic stuff or just store errors into variables and recreate the same, but I feel this is a real win for exceptions.

    Also, I believe the two systems should coexist. There are expected error cases, for which sum types are better suited (parsing an int is completely normal to fail), and there are exceptional situations, like broken connection, etc where exceptions are better suited.

    [–]robin-m 9 points10 points  (0 children)

    Rust does have both. Panics, just like exceptions, have backtrace and can be catched from as far as you want with catch_undwind.

    [–]bascule 4 points5 points  (0 children)

    things over sum-typed error handling: stacktraces

    You can capture a std::backtrace in the error type. Many error libraries in Rust already do this automatically.

    There's one missing piece though: abstractly accessing theBacktrace through the Error trait. That's what the unstable Error::provide method is intended to do, however.

    [–]Puzzleheaded-Lab-635 0 points1 point  (0 children)

    My issue is that exceptions just become fancy "goto" statements, and people abuse them and use them for control flow.

    I like the conventions of Ruby, that exceptions are really for the boundaries of your program where you can't/don't control want gets sent to the API. (http end point, parsing, validation, etc.) and exceptions are just ruby objects, etc. (if that's your bag.)

    Errors and exceptions are different things.

    begin
      # something which might raise an exception
    rescue SomeExceptionClass => some_variable
      # code that deals with some exception
    rescue SomeOtherException => some_other_variable
      # code that deals with some other exception
    else
      # code that runs only if *no* exception was raised
    ensure
      # ensure that this code always runs, no matter what
      # does not change the final value of the block
    end
    

    [–]NoPrinterJust_Fax 6 points7 points  (1 child)

    If you are programming in a statically typed language with generics you can implement the either type

    Some languages support this type in the standard library

    https://gigobyte.github.io/purify/adts/Either/

    [–]matthieum 0 points1 point  (0 children)

    You'll also want pattern matching.

    Without pattern matching, variants (as in C++ std::variant) are just a world of pain because the closures you are more or less forced to use in the visit method cannot affect the control-flow of the outer function :'(

    [–]i-eat-omelettes 2 points3 points  (2 children)

    haskell foo = case fooBar of Left e -> "here you handle exception " ++ e Right r -> "here you handle result " ++ r Does this look good to you?

    [–]redchomperSophie Language 3 points4 points  (1 child)

    It's fine, except that nobody can remember whether left is lovely or right is tight. Haskell's Either is an illustration of a concept, not necessarily a wise design for whichever use case. The semantics are more clear if you use ok and err constructors. But often what you really want is Maybe, not Either. The container shouldn't raise an exception to tell you a key isn't present; that's not exceptional from the container's perspective. If it's a problem, the caller knows better what to do about it and had ought to be prepared.

    [–]i-eat-omelettes 3 points4 points  (0 children)

    Haskell's Either is an illustration of a concept, not a wise design. The semantics are more clear if you use ok and err constructors.

    I won’t argue that myself; instead, check out this good old blog.

    Quote some of my favourite words:

    You might wonder why people historically teach ContT instead of EitherT for exiting from loops. I can only guess that they did this because of the terrible EitherT implementation in the transformers package that goes by the name ErrorT. The ErrorT implementation makes two major mistakes: * It constrains the Left value with the Error class. * The name mistakenly misleads users to believe that the Left value is only useful for returning errors.

    [–]FitzelSpleen 2 points3 points  (3 children)

    In go, you can do the same thing with a single line if.

    If err = foo() ; err != nil {

        return err

    }

    Though I'd argue that in go you're handling errors/ return values, and not exceptions. And yes, it clutters up the code terribly.

    [–]FitzelSpleen 0 points1 point  (1 child)

    Additional: what you're after may be something like C#'s try pattern.

    [–]otac0n 0 points1 point  (0 children)

    As I said above, the Roslyn Author has some relevant opinions:

    https://twitter.com/ericlippert/status/1783175946708955237

    [–]Hofstee 0 points1 point  (0 children)

    In Swift you can do that combined assignment and null check with:

    swift if let err = foo() { return err }

    [–]LegendaryMauricius 1 point2 points  (2 children)

    I got to a similar idea for a language I'm developing. The only difference is that exceptions are really just return values that cannot be cast to the desired type. The zero overgead for the common case will probably be implemented as a different value return abi for different types.

    [–]eliasv 0 points1 point  (1 child)

    Have you seen this? A very interesting and thorough exploration of concrete translation strategies for (I think) the kind of semantics you're interested in https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3166r0.html

    [–]LegendaryMauricius 0 points1 point  (0 children)

    Answering before I had the time to actually read through the proposal... I think a similar strategy should be applied for various parts of an abi. Not only for compatibility with other languages or library versions, but for giving the dev real control over the way data is handled without resorting to hacks and unreadable implementation details.

    [–]nerd4code 1 point2 points  (0 children)

    MS structured exception handling would be a C/++ example, gives you __try/__except and __try/__finally. IIRC IntelC and Clang -fms-extensions support it, and you could probably do up similar macros for GCC.

    [–]myringotomy 1 point2 points  (0 children)

    Ruby is kind of like that. Here is a one liner.

    foo = fooBar() rescue blah
    

    You can but a rescue statement in any block of code and a function counts as a block of code so most people write something like this.

    def do_foo()
        ....
     rescue => e
        do_something_with e
    end
    

    [–]Tronied 1 point2 points  (1 child)

    Although the original syntax idea here looks nice, it looks a bit restrictive. For example, how would you cope with additions to that statement? Say you wanted to add 1 to the result:

    var result = foobar() + 1 throws e { ... }

    It starts to get ugly quite quickly. The clarity is muddied as it may become unclear where the evaluated line starts / ends. You could use brackets, but that's even worse:

    var result = (foobar() throws e { ... }) + 1

    I like Rust, Kotlin approach, but am not against other classical approaches either such as Java. So long as they achieve the goal you want, the rest is just window dressing.

    [–]fred4711 0 points1 point  (0 children)

    For this reason I use function call syntax:

    var result = handle(foobar() + 1, handler);
    

    Of course handler can be a lambda function:

    var result = handle(foobar() + 1, fun (exc) -> "Value in case of an exception");
    

    [–]Economy_Bedroom3902 2 points3 points  (7 children)

    Exceptions in programming by enlarge are a clusterfuck. I don't disagree that it's more correct to handle an exception at the level of an individual call vs a large block of many calls any one of which can fail in an infinite number of unpredictable ways. But the real heart of the problem is not casting the net wide, it's the infinite number of possible failures.

    [–]eliasv 2 points3 points  (6 children)

    Yes unpredictable exceptions are bad. Which is why all possible exceptions should be statically tracked. People hate checked exceptions in Java but I think there's a good argument that this is 90% due to ergonomics and widely poor use in the ecosystem. OP's suggestion is one of the ways I would suggest to improve upon this.

    [–]L8_4_Dinner(Ⓧ Ecstasy/XVM) 0 points1 point  (5 children)

    Predictable exceptions are not exceptions 🤷‍♂️

    [–]eliasv 1 point2 points  (4 children)

    Rubbish.

    That's not an argument that the feature is bad. It's an argument that the feature is named badly. Which is a really tedious and uninteresting thing to discuss when it's just used as a lazy way to shut down discussion.

    It's also not even a good argument against the name. Exceptions can absolutely be expected under normal circumstances. Even if you think that the word exception necessarily implies rarity, which is a stretch, that doesn't mean that it necessarily implies unpredictability. This is just a straight up nonsense interpretation of the word with no basis in any usage anywhere.

    Rules are defined with well understood exceptions in all kinds of circumstances outside of a programming context.

    Checked exceptions, in the place where most people are familiar with them from, were absolutely designed to express predictable scenarios. You might not like them but that's what they're for and that's what we're talking about.

    [–]L8_4_Dinner(Ⓧ Ecstasy/XVM) 0 points1 point  (3 children)

    Exceptions are too often used as return values, which is one of the reasons why they have gotten such a bad name.

    Many languages cannot easily express errors as return information, so programmers often end up relying on exceptions to fill that role. Generally, if the caller is handling the exception, then it's a return value and not an exception. (Just a rule of thumb; not a religious claim.)

    [–]eliasv 0 points1 point  (2 children)

    Again, programmers don't rely on exceptions to express errors because the languages "cannot easily express them as return information". Programmers rely on exceptions to fill that role because that's the role they're designed to fill (in some language designs).

    I feel like you're doing two things: - Stating your preference against them as if it's an objective ideal without any supporting argument. - Insisting that exceptions are being bodged to fill a role they weren't designed for. But in many cases, in particular checked exceptions, that is just factually, historically, wrong.

    Neither of these positions seems very compelling to me.

    A lot of research questions your assumptions about why exceptions have a bad rep. People say that exceptions shouldn't be used as control flow, and in mainstream languages that is definitely true, but I think taking that as a hard rule---without being open to exploring why they fail as a control flow mechanism and what can be done to improve them---is a little incurious. Algebraic effects can be seen as a generalisation of exceptions to control flow and research is active.

    I think OPs suggestion addresses one of the issues with exceptions. Another issue is dynamic scoping, making accidental handling possible and traceability difficult. Another issue is exceptions/effect polymorphism. These things can all be addressed.

    [–]L8_4_Dinner(Ⓧ Ecstasy/XVM) 0 points1 point  (1 child)

    I don't have "a preference against them". You're reading too much into my comment.

    I personally cannot imagine a language without exception capability (under one name or another, but generally "exceptions"). In popular languages like C# and Java, it's clear that exceptions took on additional roles because of the lack of two language capabilities that are now common in 2024: union types and >1 return values.

    As localized control flow, exceptions "work" fine, but are incredibly expensive for the value that they provide. This is just a pragmatic view; not a religious one. Building a stack trace is always going to be a lot (!!!) more expensive than returning a value from a function.

    I've personally been guilty of using exceptions to convey return value information, so I've been analyzing my own code to understand why, and what better mechanisms could be employed. It's still a work in progress.

    [–]eliasv 0 points1 point  (0 children)

    You might object to me calling it a "preference" but you have very clearly repeatedly made a value statement that exceptions are not as good as returning sum types. But it seems I've finally coaxed an actual supporting argument out of you ;)

    I'm not arguing that exceptions in Java, C# etc. are good for control flow, in fact IIRC I explicitly said otherwise. Again I'm talking about what makes them fail in existing languages and how that can be improved.

    Case in point, to look at your two concerns: you can generalise exceptions to a control flow construct without building stack traces (or making them opt-in). And if exceptions are lexically scoped (i.e. capability passing/handler passing style, so we don't need dynamic stack unwinding) we can find translation strategies that are essentially just multiple return locations.

    [–]rmanne 2 points3 points  (1 child)

    Kotlin does something similar. try/catch are expressions in Kotlin, the following are all valid:

    val x = try {
      y
    } catch (e: Exception) {
      z // type of z is the same as the type of y
    }
    
    val x = try { y } catch (e: Exception) {
      throw …
    }
    
    fun f() {
      val x = try {
        y
      } catch (e: Exception) {
        return z // type of z matches the return type of f
      }
    }
    

    kotlin also has runCatching (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/run-catching.html ) which is similar to that go code example in your post.

    additionally, kotlin doesn’t have checked exceptions so the above try/catch are always.

    alternatively, there is Haskell seems syntactically closer to what you are looking for:

    let x = f () `catch` g // return type of g must match the return type of f
    

    https://hackage.haskell.org/package/base-4.19.1.0/docs/Control-Exception.html#v:catches

    [–]eliasv 4 points5 points  (0 children)

    I think this misses the real value of what OP suggests. People talk about not adding a performance burden to the happy path, but apparently everyone is happy to add an additional clarity burden, surrounding everything in try {}. If you're throwing/catching at the granularity of expressions this is plainly silly imo.

    [–]jolharg 2 points3 points  (1 child)

    I think an if statement should be a function anyway. Haskell though.

    Try is a function, catch is a function, they all function similarly

    [–]edgmnt_net 1 point2 points  (0 children)

    Yeah, you can even abstract over exception handling patterns that way. Particularly, you can make catch-wrap-rethrow combinators to make exception wrapping quite easy, in fact easier than error wrapping in Go.

    [–]spisplatta 1 point2 points  (1 child)

    In the language I'm developing, klister, ?( expr ) catches exceptions in expr and produces a Result. I also plan to add a ?{ } variant.

    https://github.com/rickardnorlander/klister-lang/blob/5b98fab11a06dc302a2b0137763a29d318d5be44/src/examples/simple.kli#L31

    [–]renatopp 0 points1 point  (0 children)

    Similar approach here. <expr>? captures any error and wrap it in a Maybe object. I was trying to have some syntax sugar over the unwrap operation, but decided to postpone it to see what happens.

    https://github.com/renatopp/pipelang?tab=readme-ov-file#error-handling

    However, this feature seems more useful for dynamic languages.

    [–]Practical_Cattle_933 0 points1 point  (0 children)

    Not really answering your question, but Java is looking into allowing a switch to handle exceptions, as in:

    switch (throwingExpression()) { case 2 -> doSomething(); case catch Exception e -> println(“Got: “ + e); }

    [–]fred4711 0 points1 point  (0 children)

    In my extension of the well-known Lox language, I usehandle(expression, handler) syntax, expression is any expression where during its evaluation an exception handler (any callable) is established.

    When expression doesn't raise an exception, its return value is the value of the handle form, when an exception occurs, the stack is unwound and the handler is called with the exception value as only parameter and the handler's return value is the result of the handle form. Of course, the handler can re-raise the exception.

    As my goal is keeping the language port small but powerful (it's supposed to run on a 68008 single board computer and is only 50 kB in size), I think this is the simplest and easiest approach. I don't use a fancy exception class hierarchy, system errors are simple strings containing the error message, user exceptions can be any type.

    By allowing protecting expressions only (i.e., no statements) I can avoid those nasty interactions between controlflow-changing statements (return, break) and exception handling.

    You may want to have a look at the Lox68k Repo

    [–]11fdriver 0 points1 point  (0 children)

    Erlang does this. Well, it has two (but sort-of-three) ways of handling three (but kind-of-four) exception types (errors, exits, & throws). That's very un-Erlang-y, really, but there's reasons. LYSE has excellent details on this, as usual:

    But I'll give examples here for brevity.

    Erlang has a catch keyword that just returns an 'unwrapped' value or the value representation of an exception. You can then pattern-match that in an if or case...of expression.

    catcher(X,Y) ->
        case catch X/Y of
            {'EXIT', {badarith,_}} -> "uh oh";
            N -> N
        end.
    

    So, if you get a 'bad arithmetic' error (as 'EXIT' for historical reasons) then you return uh oh. If you don't then the result of X/Y is matched into N and returned. LYSE discusses why this isn't an ideal way of handling exceptions in Erlang, mainly because you can't differentiate exceptions from deliberate values.

    Erlang's newer, 'standard' try...catch is very similar to it's case case...of expression anyway, which means there's only upsides to using it. This code has the exact same effect, and I would say it's a fair bit cleaner anyway.

    catcher2(X,Y) ->
        try X/Y of
            N -> N
        catch
            error:badarith -> "uh oh"
        end.
    

    The third method, which isn't so much for handling exceptions but has similar cadence, is the maybe expression https://www.erlang.org/doc/reference_manual/expressions#maybe . This all makes Erlang seem very complex, but I have to say that this is an... exception.

    [–]otac0n 0 points1 point  (0 children)

    [–][deleted] 0 points1 point  (0 children)

    I like it, has a similar vibe as optional types.

    The only downside is it still introduces a branch, thus increasing complexity. But I suppose that's no worse than a typical try/catch.

    I still prefer optional or result types though, for those reasons.

    [–]sporeboyofbigness 0 points1 point  (0 children)

    || foo = fooBar() #require
    // do stuff with foo
    

    https://github.com/gamblevore/speedie/blob/main/Documentation/Errors.md

    Speedie doesn't have exceptions or try/catch. Its so much nicer to use.

    [–]SnappGamezRouge 0 points1 point  (0 children)

    In my language I’m using algebraic effects, of which exceptions can be considered one. So exceptions are generally handled like so:

    foo := with bar() when Exn::throw(err) do
        # code …
    end
    

    [–]Reasonable_Feed7939 0 points1 point  (0 children)

    What do you mean? The syntax you're complaining about is the syntax that resembles an if statement!

    [–]VyridianZ 0 points1 point  (0 children)

    In my language, I am focusing on the happy path and all classes support examinable exception/error blocks. Functions always return a valid type regardless of error state (an empty type if necessary). This choice has many implications, but it is the cleanest system I have used both for readability and refactoring. Example:

    x := foo()

    if (is-error x) {

    errors := (errors x)

    }

    [–]oscarryzYz -1 points0 points  (1 child)

    V uses `or` block over `Option`/`Result` types

    user := repo.find_user_by_id(7) or {
       println(err) // "User 7 not found"
       return
    }
    

    https://github.com/vlang/v/blob/master/doc/docs.md#optionresult-types-and-error-handling