62 comments

[ 3.0 ms ] story [ 88.1 ms ] thread
This feels like an ergonomic improvement to Java's checked/unchecked exceptions.
After many many years I've come to the conclusion that languages that use exceptions for error handling should only have checked exceptions. As much as I have railed on Java's checked exceptions in the past, I think unchecked exceptions give you either brittle code, or unreadable code littered with gratuitous try/catch because you never know what might or might not throw.

And I don't think this would actually be that bad. Functions/methods could still decide not to propagate exceptions outward; they just have to declare what they throw/propagate.

Of course, the problem here is that the list of thrown exceptions becomes a part of a function's API (and possibly ABI); changing the implementation details of a function -- even without changing its behavior -- might require you to add a new exception to the list, which would be an API break. I expect this would lead people to often declare that their functions throw the generic base-class Exception type, which isn't great.

Functions should not be exposing their implementation and that's what makes something an error rather than just output from a function. One should not care if a function was implemented as a pure calculation, read from a file on disk, or use a network service. And that should be able to change at anytime without breaking the callers.

This means that checked exceptions and explicit error returns that are anything other than a very generic error type are incompatible with encapsulation and polymorphism.

I think the best generic solution to error handling is unchecked exceptions. Most code should not be littered with try/catch blocks and none of them should be gratuitous. Even in my largest projects the number of catch blocks can be counted on one or two hands. You don't need to know what might or might not throw -- everything can throw. If something does throw today, it could throw tomorrow. Don't worry about that. With unchecked exceptions, just concern yourself on what you can handle and where you can handle it. Most error handling is either to abort (the operation or application) or retry.

It is often important for the caller to know under what circumstances and for what reasons a function can fail, in order to react appropriately. In particular, callers want to distinguish between expected and unexpected errors, which can differ from use case to use case, and for expected errors they may want to distinguish between different expected cases, for example to decide whether it makes sense to retry the operation or not.

This doesn’t mean that the function signature has to expose implementation details. If the function is file_open(), listing FileNotFound as one of the possible errors doesn’t expose an implementation detail. If the function is sql_insert(), distinguishing between ConnectionError and UniqueKeyConflict is useful and doesn’t expose implementation details. Different error modes like that exist on all levels of an application.

A function containing file_open() or sql_insert() will expose implementation details if it leaks those errors out.

I don't disagree but most of my application is not in those leaves; it's a call stack 20 levels deep.

> A function containing file_open() or sql_insert() will expose implementation details if it leaks those errors out.

The correct thing to do in that case is to wrap/convert those errors to errors that are appropriate to that function’s interface contract. Exception types are an important part of a function signature, and should be designed accordingly, not be propagated blindly.

With unchecked exceptions, you are leaking implementation details dynamically, and you don’t even know it/can’t control it, because they are not statically checked. If you care about leaking implementation details, unchecked exceptions are arguably worse.

> I don't disagree but most of my application is not in those leaves; it's a call stack 20 levels deep.

I regularly have such failure mode distinctions in higher-level program logic as well. With unchecked exceptions, you often aren’t even aware what failure modes you have.

> The correct thing to do in that case is to wrap/convert those errors to errors that are appropriate to that function’s interface contract.

Those, however, tend to be useless. What you end up doing is placing the real error untyped into new the exception/error and that's where the real meat is.

I can create an application that could retry an operation if it sees a particular kind of network error. Does it also work for the dozens of custom errors used by the interface contract of every single method? That's impossible to even know. How do I handle dozens of different SomeModuleError types?

> you often aren’t even aware what failure modes you have.

Again, that is the point of encapsulation and polymorphism. Maybe the implementation doesn't even exist at compile time in the case of a plugin.

I don't want to have to re-write my entire application every time some implementation detail changes anywhere. If you don't want to do that, then you're have to transform your errors/exceptions into something useless.

What I want to focus on what kinds of errors I can handle and where I can handle them. That has nothing at all to do with where the errors are generated.

Unchecked exceptions make sense for precondition checks and the like, that is, to signal bugs in the application. Otherwise I agree, checked exceptions are the way.
Alas the java community disagrees, and this collective decision should be carefully analyzed and respected

Because it represents the conclusion, despite ample guidance and dogma by Sun, of the mass market millions of java programmers, resulting in second generation (groovy) and third generation (kotlin) JVM languages that dropped checked exceptions entirely.

It might be the languages error handling syntax (try catch) led to ugly code (and I always contended that local vars declared in a try block should have been lexically visible to catch blocks)

It might be that javas std lib had a poorly designed exception pattern. It could be that implementing exception classes was annoying.

But I think there is other reasons. In large scale enterprise development, checked exceptions start to pollute and permeate code that otherwise doesn't want excessive type system exposure from the linked /imported libraries.

What results even with checked exception lite is that "exception barriers" that catch all exceptions and repackage them are established at major system boundaries. Checked exception enforcement would just create more of these IMO.

The wet dream of checked exceptions was encouraging APIs that had a thorough understanding of all the error conditions and could use checked exceptions to enforce a good handling recovery API usage pattern

In reality it's the halting problem: too hard to predict l all the error conditions (that isn't really known until version 3 or 4 of the API and by then the API signatures are set.

Let's look at something simple like json serialization. Yeah there's a litany of parse errors and the like.

But there is ALSO the issue of disparate sources of the json. File errors from file sources. Network errors from streamed communication channels. Your general stream or file interface isn't that simple of an abstraction when source specific errors raise their heads.

The abstraction become leaky real real fast.

I'm one of the naysayers as an experienced Java developer that supports checked exceptions. I find the move to runtime exceptions very disheartening though I will say that in a world dominated by web applications with a clear request/response cycle it's very easy for the container/application to implement a high-level error-handler so I think this skews people's sense of the value proposition of checked exceptions. I've done a fair bit of work over the years writing daemons of various types and having checked IO exceptions among others has been a lifesaver. I also feel that vilifying use of generic exceptions (IOException, SQLException) in favour of implementation-specific exceptions was a mistake. If instead, we had encouraged a smaller set of mostly platform-defined exceptions I believe people would have found the exception "noise" to be lesser. While there are cases where a custom exception sub-type can be useful, in many cases they are not. I agree with the grandparent comment that we may have been better off without runtime exceptions at all, leaving us with simply checked exceptions and errors.
Checked exceptions were an ideal. From a halcyon Valhalla of fallen unix programmers trying to impart their wisdom to the ignorant masses.

I too would like to live in this world.

However, while I don't have specific exceptions at the tip of my tongue, Even sun failed in many of their jdk apis to produce a good checked exception design.

"New", no, but "not as fully explored by programming languages as other paradigms" yes. A very similar approach to the "explicit propagation" version was used in Midori:

https://joeduffyblog.com/2016/02/07/the-error-model/#recover...

I highly recommend reading that entire blog post -- it's so well written and well researched, and changed my mind on a lot of things about error handling.
I didn't get the same impression.

Midori promised to never-ever-ever throw unchecked Exceptions. But when something goes wrong, it just crashes? Sorry, "abandons"?

If it's the preferred strategy, fine, but it shouldn't be made out to be some bastion of correctness.

The article states the following things about error handling: Implicit error handling > Simpler to get started, thus ideal for novice developers. Explicit error handling > More to learn & write, thus better suited to seasoned developers.

These two statements seem odd. I believe an equally valid argument is that explicit error handling, in its explicitness, makes it easier for novice developers, as they no longer need to model their library function's behavior in their head. However, I believe the most reasonable approach is opt-in explicit error handling.

To use the lingo in the article.

- Should function signatures contain errors? (opt-in, lean yes)

- Should call sites explicitly handle errors? (opt-in)

- Should libraries opt-in to function signatures with errors? (probably yes)

As a client, almost every time, I want my error to bubble. If any step in my golden path fails, with the exception of transient http calls, the entire flow is probably done for. Do my top level functions need to explicitly state their errors? Probably not. Do I want library functions to do this? Probably.

I think this context is important for developer experience. And since I believe this varies on context, I believe function signature errors should be opt-in.

I actually think that having both possibilities, as in the OP, is the most complex solution of all, especially if you need to go hunting for a `?` that could be anywhere on your screen.
The problem is that making function signature errors optional effectively "colors" all the functions. If I want to write a function with errors in the signature, then what if I call a function that doesn't have the error in the signature? In the presence of dynamic dispatch (ie. interfaces), the compiler can't infer the errors. Thus errors in function signatures can be required or not, but they can not be optional.

(Please point out any of my mistakes.)

This is a good callout. I don't have any disagreements but will have to think about this. Maybe my proposal is not reasonable for the reasons you provide.
This is decent, but I don't like that the compiler only warns if you don't handle things properly. This should be a hard compiler error.

But overall I don't agree with the premise that explicit error handling is difficult for new developers, and is better suited for seasoned developers. Handling errors is a part of programming, period, and we do new developers a disservice by trying to hide that fact.

I think Rust-style error handling is fine, where you can handle them at the call site, or use `?` to propagate to the caller, and it's good that propagation requires that the current function returns a Result with a compatible Err type. (I do wish not handling a Result was a hard error, and not just a warning via `must_use`.)

> I do wish not handling a Result was a hard error, and not just a warning via `must_use`

It is possible to turn any Rust warning into an error with the use of the `deny` attribute. In addition it is now possible to configure the behavior of lints in the `lints` section of Cargo.toml.

Author here, thanks for the comment, it is indeed a hard compiler error not a warning. Bad wording.

It is a fair point too about new developers / seasoned developers. Maybe citizen developers is a better phrase? I've encountered many non-professional developers who just hack together things and really couldn't stand verbose error handling. And for what they do, frankly, I don't blame them. I'm also fine writing bash scripts without a decent type system.

I don't think you can lump Java with the others in this comparison, the checked exceptions in Java make it different. Specifically this:

> Function signatures do not contain errors.

Does not apply to checked exceptions in Java. The possible exceptions thrown are definitely part of the function signature and matter when implementing/overriding/etc.

Also I really dislike phrases like this in these sorts of comparisons:

> thus better suited to seasoned developers.

It's a real oversimplification, and can be interpreted as an insult on issues like this where there genuinely are advantages to either approach. More often than not, these statements imply you see yourself as one of those "seasoned developers". And you'd better be damn sure you got everything right if you start self-labelling like that.

You're completely right. Here's the example function signature from the blog post:

    fn greet() (string) (error)
In Java, you can write exactly the same thing:

    String greet() throws Error
The "implicit error propagation also looks exactly the same.

    fn main() () (error) {
        s := greet() // <-- Implicit error propagation
        println(s)
    }
In Java:

    void main() throws Error {
        var s = greet();  // <-- Implicit error propagation
        System.out.println(s);
    }
The case with a question mark after the error is the only case not covered by Java.

The final case, with explicit error handling:

    fn main() {
        s := greet() // <-- failure result can not be propagated
        println(s)
    }
Also looks exactly the same in Java:

    void main() {
        var s = greet(); // <-- compilation error: must handle Error
    }
EDIT: ok, let's mention also RuntimeException! In Java, you don't need to declare those (and subtypes) in your signature (but you can) and even if you do, you don't need to handle them. That's because the original idea was that RuntimeException was to represent programmer mistakes that should never be caught. The Java community ended up using it exactly the opposite way because everyone complained about explicit error handling being too much of a chore, so making most Exceptions subtype RuntimeException became common so now you can't assume that you don't need to catch them (but the compiler won't force you to)... other JVM languages like Groovy and Kotlin just eliminated checked Exceptions completely for that reason. It's a bit amusing to me how "checked errors" in general has made a comeback in Go and Rust, and in many newer languages like Zig (and now this one) as well. I think that the way Rust does it seems to be the best "compromise". It's almost like Java's checked Exceptions, but it's added sugar on top to make it much easier, while still explicit, to propagate errors. It's a tiny difference , but it seems that tiny difference makes ALL the difference for people (just see how people love Rust and its error handling, but loathe Java's).

Oracle guideline: https://docs.oracle.com/javase/tutorial/essential/exceptions...

(comment deleted)
> It's a bit amusing to me how "checked errors" in general has made a comeback in Go and Rust

Go is... well, Go.

On other languages, it came back because they have polymorphism on the error type. Java is a huge mess because it has no implicit types nor most instances of polymorphism.

> Java is a huge mess because it has no implicit types nor most instances of polymorphism

Heh? What instance of polymorphism does it lack? Also, depending on what you mean by implicit types, Java has type inference nowadays.

IIUC, for checked exceptions Java lacks parametric polymorphism (generics).
Ah. But that’s also not true, you can do something like `throws T`.
I assume that the poster meant that Java could have had `Result<T, E>` return values like Rust if it had launched with parametric polymorphism, instead of having a layer of the type system dedicated to errors.
It still can have it, see e.g. Vavr, or other functional libraries.

Edit: Not sure why the downvote.

This works in Java today with proper pattern matching (I left out the complete name from the permit clause, it won’t compile as is):

  sealed interface <T,E> Result permits Ok, Error {
    record Ok(T val) implements Result<T,E> {}
    record Error(E err) implements Result<T,E> {}
  }
Man I hope Vavr survives. It is an oasis of rationality and I'm disheartened that https://docs.vavr.io/ appears to be down. Its Set implementation actually has union and intersection (!!!)
Sure, and it's great that this has become possible (a while ago now). My remark was about Java ~1.0, as by now, the ship has long sailed and it's unlikely that many people will start adopting this style.

(and no, I have no idea why you're being downvoted, please take my upvote)

It works, until it doesn't. Exceptions are the one place in Java that supports union types, if you have code that can throw both Exception1 and Exception2 then Java can keep track of whether they are thrown separately:

    class Exception1 extends Exception {}
    class Exception2 extends Exception {}

    interface ThrowingFunction {
      void apply() throws Exception1, Exception2;
    }

    class Foo {
      public void doFoo(ThrowingFunction f) throws T {
        f.apply();
      }

      public static void main() throws Exception1, Exception2 {
        new Foo().doFoo(() -> {
          if (false) {
            throw new Exception1();
          }
          if (false) {
            throw new Exception2();
          }
        });
      }
    }}
However, if you pass them through a generic then they will be flattened into the closest common ancestor:

    class Exception1 extends Exception {}
    class Exception2 extends Exception {}

    interface ThrowingFunction<T extends Throwable> {
      void apply() throws T;
    }

    class Foo {
      public <T extends Throwable> void doFoo(ThrowingFunction<T> f) throws T {
        f.apply();
      }

      // Fails to compile, because Exception is still unhandled
      public static void main() throws Exception1, Exception2 {
        new Foo().doFoo(() -> {
          if (false) {
            throw new Exception1();
          }
          if (false) {
            throw new Exception2();
          }
        });
      }
    }
Ah, neat! I think the last time I looked you couldn't, but it was ages ago; I'm not surprised to have misremembered and even less surprised if it's changed.

Is it possible to provide a map method that re-throws whatever is thrown by its argument, without widening?

> What instance of polymorphism does it lack?

1) Higher-kinded. List<Integer> is OK and List<T> is OK, but T<Integer> is not.

2) Dispatch to the right method based on param types. If your class exposes foo(Apple) and foo(Banana), it will not route a Fruit to either of those methods even if it is one of those specific types.

3) Return type polymorphism, which is probably the most relevant to the error-handling discussion, e.g. in this example:

  fn greet() {
    fail "not greeting today"
  }
in another language, fail could be polymorphic over its return type - that is, if called from a function returning an Optional<>, fail itself could choose to return Optional.empty(). Or it could return the failed version of a CompletableFuture<>, or even throw a runtime exception depending on how the language was implemented.
> It's a bit amusing to me how "checked errors" in general has made a comeback in Go and Rust, and in many newer languages like Zig (and now this one) as well.

For what it's worth, checked errors have been part of the ML family of languages (some of the immediate ancestors of Rust) since pretty much forever and of LISP even longer.

Still, let's give credit where it's due: very few people used them before Java.

> I don't think you can lump Java with the others in this comparison

Fair point, I've removed Java from the examples. Apologies, I haven't used Java for ages and relied on my memories. That said, I understand they are not used much in practice because changing a checked exception's type is a refactoring nightmare. Or are they?

> Also I really dislike phrases like this [...]

Okay, help me do better, what about citizen developers? I mean the kind of person who explicitly never wants to be a developer, yet there s/he is developing a program. I called them novice in this page, and the people I have in mind would not be offended at all.

And yes, compared to them, someone with a computer science background and 10 years of coding experience is a seasoned developer. Open to suggestions.

[flagged]
(comment deleted)
This feels a bit like Zig's error handling which seems really nice.

To rewrite the example in Zig (I am no Zig expert so feel free to correct/improve):

    const string = []const u8; //just creating a type alias to make the signature below have string 

    //Zig
    fn main() !void {
      s = try greet();
      try std.print("{s}\n", .{s});
    }

    //Boomla
    fn main() () (error)? {
        s := greet()?
        println(s)
    }

    //Zig
    fn greet() !string {
        return error.NotGreetingToday
    }

    //Boomla
    fn greet() (string) (error) {
        fail "not greeting today"
    }

    //Zig
    fn main() !void {
        s = greet() catch {
            // Error handler block
        }
        try std.print("Hello, {s}!\n", .{s});
    }

    //Boomla
    fn main() () (error) {
        s := greet() err { // Error handler block
            fail err
        }
        println(s)
    }
Both of them look very similar. Boomla uses `?` instead of `try` and `err` instead of `catch`, but the big area where they differ is that errors in Zig are enums that you can switch on - and rely on exhaustiveness checks to know that you're handling all the types of errors that could be coming from your call stack.

    fn main() !void {
        s = greet() catch |err| switch (err) {
            error.NotGreetingToday => //handle this error
        };
        try std.print("Hello, {s}!\n", .{s});
    }
And you know that you've handled the possibilities. Maybe you even find things that are bubbling up that shouldn't be and should be handled further down the stack.

With Go/Boomla, errors are often just strings and there's no way of knowing if a library has created a new error case or easily get a complete list of the errors that might be returned. In fact, since they can just be strings, it would be impossible to know since the strings could be created programatically.

If an error is bubbled up a few levels, even if you explicitly know to look for an error, you might not know what kinds of errors you should be looking for if they're just strings. If I'm saving something to a database and there's a network error, I'll likely want to retry that. If I'm saving something to a database and it's trying to save a string to an integer column, there's no sense in retrying that - it'll just keep failing. But when errors are just strings, it's hard to know what the error is ahead of time. It then becomes a game of whack-a-mole where you log the errors you get back and then create branches for the ones you've seen (and you hope no well-meaning person changes the string without realizing that you're relying on it).

Personally, I like my IDE being able to generate all the error branches that are returned from what I'm calling. I can see that I'm handling everything the way I want it to be handled. Sometimes it means that I notice something bubbling up that should be handled further down and I can just handle it there instead. While Go talks a lot about how error handling is important, it's also important to know what the error is. I've seen a lot of Go that's just `if err != nil { //all errors are the same }` (or even just returning it up the stack where you're less likely to be anticipating random strings).

It's also great because if an error is no longer returned, Zig will have a compile error so that you take out the dead code. How many people are catching exceptions or comparing error strings that aren't returned anymore?

Boomla is definitely onto something here: explicit error handling while allowing errors to be propagated up if that's what you want. I first saw this in Zig (and great languages often independently implement similar stuff). I think the fact that I can know that I'm ...

(Author) Thanks for the Zig examples, great comparison! I'm puzzled how Zig can do exhaustiveness checks in the presence of interfaces (dynamic dispatch). A quick google search suggested Zig may not have them?

The problem is, having dynamic dispatch, I would think the resulting error types can not be inferred, which means they must be explicit in function signatures. As a result, we are back to Java checked exceptions with its refactoring hell.

So that seems to be a hard tradeoff. Or please correct me.

Just for clarification, Go/Boomla errors can also have dedicated types, so you don't need to compare error strings. Yet it is correct that you can not tell if you have exactly covered all possibilities.

It looks like the error types need to be explicit in the function signatures in interfaces, but not in the function signatures of the implementations of the interface or in the signatures of anything calling those interface functions. That would give the compiler the information it needed without getting into the Java checked exception hell, yes?

Again, I'm not a language designer or a Zig expert. But here's an example in my head of what something Boomla-ish could look like (noting that I haven't seen Boomla before today so it might look more like Go).

    type SqlRunner interface {
        Select(string stmt) ([]interface{}) (SQLSyntaxError, SQLTimeoutError)
    }

    fn getData(runner SqlRunner) ([]interface{}) (error) {
        if (day == Monday) {
            fail NoMondays
        }
        return runner.Select("SELECT * FROM table")
    }
Any implementation of SqlRunner can only fail with one of those three errors or return a slice of interfaces. The compiler can infer which errors getData can return - NoMondays, SQLSyntaxErro, or SqlTimeoutError. Even if someone adds another error to the interface, the compiler can still infer it.

What is nice is that wherever you're handling the error would now need to be updated - because you're now not handling one of the errors you're supposed to be handling. It doesn't put you into refactoring hell like Java where every method signature needs to be updated, but it does alert you where your code is now wrong because you aren't handling something.

That seems like it'd work without being problematic like Java's exceptions.

If it still seems like a problem because maybe you want to implement SqlRunner with different errors, I might wonder if it's really the same interface. Certainly if you wanted to implement it as `Select(string stmt) (int)` we'd both agree that's not the same interface. Your caller isn't expecting an int back. But if they aren't expecting a WontSelectId5 error back, isn't that kinda the same thing? If I have a SqlRunner, I might think "I want to retry on SQLTimeoutErrors and show the user an error on SQLSyntaxErrors since there's no point in retrying that." What do I do with an unexpected WontSelectId5? If I'm expecting it, I won't retry it.

If you really want to keep the interface open, maybe you could use `anyerror`. Zig has a special `anyerror` to match any error:

    Select(string stmt) ([]interface{}) (SQLSyntaxError, SQLTimeoutError, anyerror)
Everything upstream would still just have (error) in the signature and the compiler could still help you out - to an extent. For example, your switch could look like:

    switch (err) {
        SQLSyntaxError => a,
        SQLTimeoutError => b,
        anyerror => c
    }
That would allow for an interface implementation to have its own custom errors while the interface still defines types for the ones that might happen most often across implementations.

I'm curious if you'd want the interface to have an `anyerror` addition. It allows for random errors to be returned, but maybe that makes it a different thing or that the interface needs to be updated (and updating the interface wouldn't be a refactoring hell since everything up the call stack would would get inferred by the compiler.

> Go/Boomla errors can also have dedicated types, so you don't need to compare error strings

I guess I just saw `errors.New` a lot in Go, but part of that might be that I haven't used Go in the past few years. It sounds like maybe there's a shift to using types and `errors.Is` to check?

So the error type can be left out when the compiler can infer it. If it is left out, and the compiler can't infer it, Zig will fail to compile and provide an error message pointing to the missing error type information. Thanks for the clarification, this totally makes sense.

> That seems like it'd work without being problematic like Java's exceptions.

It's definitely a _huge_ improvement. I think the usecase with returning different errors is significant when you are mixing lots of 3rd party components.

I'm building a platform, which defines several interfaces that components must implement, so I believe those intefaces would "erase" the concrete error types and everything would just end up being anyerror. That said, I could still see concrete error types still being used in some lower level logic, where function calls don't cross such an interface boundary.

As for projects that don't bother with implementing 3rd party interfaces, I can totally see how that may be a blessing.

Also:

    echo "Hello"
    echo 1>&2 "World"
It makes a _ton_ of sense for functions to have two "streams" for output/returns (stdout / stderr).
Essentially the same as continuation passing style with a choice of continuation to return with.
I'm a CPS person, so I always struggle to understand why this answer isn't the default. sugaring would be

a) a second implicit argument for the error return

b) an error() exit to go along with return()

c) a call-with-handler() to override the default of passing your handler to your child

that...looks alot like exceptions

Is this really a new approach? On a cursory look this seems like implicit error propagation with checked exceptions. I am Also curious about authors presentation of the topic. To me, an important feature of error handling design is whether fallible contexts are marked (e.g., with try statement) or not.
(Author) Thanks indeed checked exceptions have been brought to my attention in this thread, and in theory they are indeed very similar, except I understand changing the type of a checked exception causes refactoring hell, so they are painful to use in practice.

The other aspect is ergonomics. A major problem I have with try..catch blocks that the article doesn't touch on is that they created nested blocks, with their own scopes. This either results in nesting hell, or the variables that are assigned inside the try block must also be defined outside the try block. This makes the code simply unpleasant to write.

Have you looked at the Swift error model? I really like their design. They use a dedicated try statement to mark call sites that can fail — note that try is not the same as try...catch — Swift has an additional block construct for catching errors. This design makes sure that you always know where errors can occur when reading the program code, but avoids all the ergonomy issues you mention.

Your model sems to be every similar to the traditional implicit model used by languages such as C++, only that you allow switchign between the implicit and explicit error propagation. I am not sure how much this is useful in practice, as it creates inconsistency.

As I understand Swift's error handling falls straight into the explicit camp. That's all good and I love it.

My problem is that it seems it's not for everyone at every stage of their journey. When I teach software development to a musician, it's just in the way. I needed a language that works for professional developers doing low-level work, but also for citizen developers doing their first round of hacking.

Originally, I thought implicit error handling is not for me. I will add support for those who hate explicit error handling and not use it myself. I'm at a point in my journey where I started questioning the benefits of explicit error checks (ie. Swift's try keyword or ? in Boomla) when all I do is propagate errors.

I'm thinking that maybe it would be interesting to start with implicit error handling, and turn on explicitness where you need it. Existing error handling approaches don't let you do that. I see the big idea in this approach that you can turn it on incrementally, with no far reaching effects. How you handle errors is a local implementation detail of the function.

Of course we will see, I might end up sticking to the explicit model, but for now I enjoy it. Luckily, migration won't be an issue, I can just return to using explicit error handling if I change my mind, and current code will keep working as is.

Maybe it's just me, but the benefits of implicit error handling in these examples seem really minor. Is it really worthwhile to have that as a feature just to save few ? in the function body?
It didn't exist originally, but for high level programs like a TODO app that is mostly html templates, those question marks really don't add much value. Plus every time I showed code snippets to web developers, they got hung op on them.

Also, I wrote Go for like a decade, I love the explicitness of Go's error handling, but I still hate the `if err != nil {...}` checks.

Another reason was that Boomla will have a complete nocode UI, and I really don't want to clutter it up with explicit error handling.

That said, it's still an experiment, I guess.

I find it ignorant to categorize languages like this. There is nothing about javascript, php, python or java keeping you from using the explicit approach described there.
Maybe I'm missing something, but this just seems like a less general construct than algebraic effects; for instance, see the exn effect in Koka: https://koka-lang.github.io/koka/doc/book.html#why-effects

Is there some benefit of this approach compared to algebraic effects?

Implementation simplicity?
Could you shed some light on the degree to which it's simpler? I've heard that algebraic effects are complex but I don't have much of a feel for why that is.
Agreed, it's pretty clearly a subset of algebraic effects. In fact, I'm fairly certain that I've seen examples similar to OP as the hello world of algebraic effects a few times.
(Author here) Yes, that's correct. It's an algebraic effect elevated to a dedicated language feature.

The benefit is that it is simpler than having full-blown algebraic effects, both to learn and to actually use. Probably not for you, but for most people, and definitely for the non-professional developer kind.

(comment deleted)
(comment deleted)