Stream: beginners

Topic: Results and Exceptions


view this post on Zulip witoldsz (Sep 11 2024 at 13:38):

Hi! I would like to ask you about the general idea regarding two kind of errors/exceptions:

I am not sure I could be much more descriptive than what I've learned from Scott Wlaschin, especially:

This is also something I do think a lot. For example: should an SQL library return a Result type from a SELECT * FROM …, describing errors like DatabaseConnectionError or TableNotFoundError?

From my perspective, when I code a µservice, the only reason for these errors is that the infrastructure is down or failed to provision the database during deployment. There is absolutely nothing I can do about it in my µservice other than just explode and let "the infrastructure people" fix the deployment. So, in my very case, when I code using F# PostrgeSQL lib, I do know the query functions can throw, but I would not do anything about it. Especially I would not like to annotate my own functions with DatabaseConnectionError all over the place.

On the other side, if I was to write a database browser, I would not like to "panic" if user provides incorrect database URL or if the table is missing. That would have to be handled as a regular error in-place.

Is the try/catch style of exception possible in Roc? Does it have it's place? What is you say about this? What would you do in both cases described above?

view this post on Zulip Isaac Van Doren (Sep 11 2024 at 15:28):

The new ? operator makes it much easier to implicitly chain results together so that you can get the “let someone else deal with this” behavior. Every function still needs to be wrapped in a Result, but you can let the errors flow freely without thinking about them. If you want to handle some errors, you of course still can by omitting the ?. This is not quite the same as exceptions, but I think it lets you get most of the nice qualities of exceptions with the big upside that errors are normal values. It seems very unlikely that exceptions will be added to Roc.

view this post on Zulip Brendan Hansknecht (Sep 11 2024 at 15:29):

We have Result for the default error handling type. I would expect essentially all libraries to return results that include things like DatabaseConnectionError.

Roc also has crash. Libraries are pushed to never use it. The main exception is truly impossible states.

There is no catch for roc crashes. Though the platform can clean up state and keep running.

If you don't want to handle database errors, I would expect the application (not library) to explicitly crash when give one.

That said, it is much much more common to just propagate the error and move on.

view this post on Zulip Sam Mohr (Sep 11 2024 at 15:36):

I agree that libraries should not use crash. The only place I've used it in a library is in Weaver, the CLI parsing lib. If your CLI parser has invalid configuration, I provide an assertValid function that calls crash to quit the entire app early. This use of crash is only viable because it's used as a way to ensure that code is written correctly, and the app shouldn't be running if your CLI config is invalid.

Even then, there is still a finishWithoutValidating function that allows the user to handle the error themselves without crashing. In short, crash is for when you may as well quit running the app.

view this post on Zulip Sam Mohr (Sep 11 2024 at 16:05):

I was actually reading this Exceptions vs. Errors article yesterday, and it brought up the same concept. Based on what my coworker told me about what Rich said in this Hybrid-Level Programming talk from SYCL 2024, I think that app authors that own their platform code could feasibly have Roc do "error handling", and the platform do "exception handling".

Error handling is what Roc currently does: if expected failures occur during execution, then we represent them with error tag unions in Results, and optionally propagate errors when a function thinks a parent should handle said error.

"Exception" handling happens in the platform: if in basic-webserver the platform gets an unmanageable error from the DB, that isn't returned as a Roc Result, no return even happens! We just send that error to be handled on the Rust side without ever finishing the Roc handling code. This is only feasible once we make custom platform code simple enough for app authors to want to own themselves, as currently they're strongly incentivized to currently just use a Roc team release.

view this post on Zulip witoldsz (Sep 11 2024 at 16:33):

I was actually reading this Exceptions vs. Errors article yesterday

I've read it just now and I cannot agree with it. IMHO, the author is providing a false dichotomy: exceptions vs results. This is wrong: exceptions and results are complement to each other. It's why I do recommend Against Railway-Oriented Programming (hint: a subversive title) because it highlights the most important difference.

After second (third?) thought... I do like the Haskell option. There is no place for exceptions in pure functions. Exceptions can only happen within IO operations (during execution). Roc is a pure functional language with no IO in itself. So it does make sense there is no way to throw.

ALSO: from my initial post in this thread - no one can tell if a specific function (like database query) should throw or return a result without a context (writing an application backend service vs a database browser). That means, we should pick a right platform for a right job. Even better: we should be able to easily write a right platform for our specific application. This is what I am waiting for. To be able to craft a very customized, tailor-made platform for my backend services ONCE, and then focus on business logic using Roc :heart:


This is only feasible once we make custom platform code simple enough for app authors to want to own themselves

Amen to that! :wink:

view this post on Zulip Sam Mohr (Sep 11 2024 at 16:36):

Yeah, that article was a whole lotta "but unwrap is tempting", which is just not true haha

view this post on Zulip Sam Mohr (Sep 11 2024 at 16:37):

Yep, same page!

view this post on Zulip witoldsz (Sep 11 2024 at 16:38):

Brendan Hansknecht said:

I would expect essentially all libraries to return results that include things like DatabaseConnectionError.

That would be so convenient for one use case an so inconvenient for other. Since it is not possible to make an universal judgement, I can see a problem for a Roc-land database drivers, as opposed to providing them through a platform which can be adjusted to focus on an application specific needs.

view this post on Zulip witoldsz (Sep 11 2024 at 16:46):

witoldsz said:

It's why I do recommend Against Railway-Oriented Programming (hint: a subversive title) because it highlights the most important difference.

To highlight the essence:

So, in this post, I’m going to lay out reasons why you shouldn’t use Railway-Oriented Programming! Or to be more precise, why you shouldn’t use the Result type everywhere (because ROP is just the plumbing that is used to connect Result-returning functions).

view this post on Zulip Kilian Vounckx (Sep 11 2024 at 16:54):

I belief roc makes working with errors as values just much easier. One thing is the ? operator, but another is automatically accumulating error tags so that you don't have to keep wrapping errors. If you want to handle specific errors near your main function, then you can just pattern match against those tags that weren't handled yet

view this post on Zulip Brendan Hansknecht (Sep 11 2024 at 18:12):

I think we need more clarification around crash. There is a reason it is named crash. While a platform can manage to recover. There are limitations around how crash can be recovered from. It is not a replacement for generic try/catch. Trying to use it as such will not be nice.

I think the most important piece to remember is that a package/library does not know what platform it will be used for. This makes crash exceptionally dangerous. If you crash in a library, you should assume that it will take down the entire user application. As such, crash in libraries should really not be used for things like DatabaseConnectionError. crash in libraries should only be used for impossible states. If a user hits a crash in a library, expect them to file a bug. Obviously there are exception (especially if it is explicit in the api that some function might crash), but it should not be used lightly.

The application on the other hand, knows what platform it is built for. It also knows how it wants to handle exceptional cases. If I am writing and app for basic-cli, crash is 100% unrecoverable and will end the application. If I am querying a database, I may want to retry a handful of times before crashing. If the library, automatically crashes, I simply can't use that library. If I am writing for basic-webserver, crash will never kill the application. basic-webserver general pushes the application to just crash. That will return a 500 to the end user and life will move on. As such, DatabaseConnectionError is probably fine to be converted to a crash in basic-webserver. This conversion to a crash, should almost certainly be done in the application. It is trivial for an application to convert result error types into a crash if they don't want to handle the error. I think it is important for this to be an application decision.

There is the case where the platform is custom to the application. In that case, crash may have a lot more flexibility, but it still is exceptionally limited. After a crash, recovery has to be done in the platform. The entire roc call stack and current state is lost. There is no crash, fix, and then continue where you left off. The roc state is broken. As such, platforms generally only have the ability to continue running with a fully reset state. This is really terrible if you want to do any sort of actual error recovery with logic like is done with try/catch. With a lot of hacky and brittle logic, more could be recovered, but it is a dangerous land to play in.

TLDR: crash in roc is not try/catch. I highly advise thinking about it as much much more severe.

Also, Roc is trying to make Result and error unions a lot nicer to use. They can feel very similar to exceptions if you need them to.

view this post on Zulip witoldsz (Sep 12 2024 at 14:00):

Brendan Hansknecht said:

I think we need more clarification around crash.

I do not think so. The name reflects its purpose quite well. OK, it might not be super clear for non-FP people that one of it's goal is to please type checker in some scenarios of impossible or near-impossible situations, but overall I would not mix the "What is crash for" into the Results and Exceptions thread.

Kilian Vounckx said:

One thing is the ? operator, but another is automatically accumulating error tags so that you don't have to keep wrapping errors.

Well, sure it helps a lot, but still: if you want to model your domain, you do not want to mix business logic with infrastructure. If I create a

it looks OK, but how can I avoid being forced to add […, DatabaseConnectionError, InvalidQuery, MissingArguments, Etc...] to each of these?

Roc's ? (or try if the change is a go) helps, but it won't fix the functions signatures issue, or I am missing something :thinking: I am sure I do, it's all a speculation, didn't have the pleasure to try it yet :sad:

view this post on Zulip Brendan Hansknecht (Sep 12 2024 at 14:10):

witoldsz said:

it looks OK, but how can I avoid being forced to add […, DatabaseConnectionError, InvalidQuery, MissingArguments, Etc...] to each of these?

If it is your application and you never want to handle those, you can just crash in those cases.

If you want to group infra errors to be less noisy, you can wrap them:

InfraErrors: [ DatabaseConnectionError, InvalidQuery, ... ]

queryCustomer: CustomerId -> Task Customer [CustomerNotFound, InfraError InfraErrors]

If you just care about a cleaner type but are fine with returning them flat, you can merge tags (note, syntax is going to change to make this more clear):

queryCustomer: CustomerId -> Task Customer [CustomerNotFound]InfraErrors

Could also just use _ for the errors you don't care about displaying in the type.

queryCustomer: CustomerId -> Task Customer [CustomerNotFound]_

view this post on Zulip Brendan Hansknecht (Sep 12 2024 at 14:13):

if you want to model your domain, you do not want to mix business logic with infrastructure.

I'm not sure this is actually a full truth. Often times, business logic has to handle infrastructure failures via retries, backup endpoints, or at least logging.

view this post on Zulip witoldsz (Sep 12 2024 at 15:34):

Brendan Hansknecht said:

Often times, business logic has to handle infrastructure failures via retries, backup endpoints, or at least logging.

Sometimes yes, but most of the times I handle these errors elsewhere. Business logic is business logic, infrastructure can be separated and hidden.

view this post on Zulip Brendan Hansknecht (Sep 12 2024 at 15:51):

That's a fair separation of concerns


Last updated: Jul 05 2025 at 12:14 UTC