@@user-uf4lf2bp8t I'm pretty sure pattern matches and function calls both have gotos in them if you strip off enough layers of abstraction To be clear, i think the abstractions are good and was mostly just being silly saying everything is a goto.
@@IsaacHarrisHolt Throw could take you anywhere in the same way return could take you anywhere. In reality, throw works like a super return, it is only going to take you up the call stack, not to some random place in the code that has a try/catch. Saying it can take you 'anywhere' always makes me assume the person has never actually worked with exceptions.
Yes, it can only take you up the call stack, but unless you have visibility on the whole call stack, you can't see where that error is going to pop out. Also, you don't KNOW which functions are going to throw, which is the whole problem.
Also, people forget that errors as values is something that it was used in C originally and people invented the try/catch because people got tired of handling errors as values. so is just the pendulum is swinging back to the other side.
Exactly. People just easily forget the past. We once were very happy with simple errors as values returned from functions, and this comes from even before C, it was just the simplest, most natural way of doing things considering how computers actually operate with data under the hood... yet for some reason someone decided that we needed exceptions. Now, we're coming back to errors as values, but, yet again, just like with exceptions, there seems to be a need to overcomplicate the system when you can just return a simple integer value for the error... the pendulum is indeed swinging back, but with each swing, it appears that the artificial complexity obtained in one swing is carried over to the next. Let's see when we finally come back to simple integer return types for errors and no structs with Ok and Err return types or anything like that.
@@tiranito2834I personally think it's good we're not using integers anymore since enums makes the code more readable and less hacky. With integers you can introduce infinitely many variants without a way to track them without a convention in place, whereas enums enforce keeping all variants within a single declaration, so that's a win for readability.
I'm on the enum side of this argument. Even if they're functionally the same thing, having a named, pre-defined set of values your function can return is infinitely better than just returning "some number", and not every C program follows the same standards. Argon2, for example, returns -1 through -36 ish for their errors, whereas other libs may use positive ints.
The only thing that exceptions have over errors are values is the fact they always come with a call stack. When something goes wrong, you know exactly which file to open and in which line to look, or at least where to put a breakpoint. I would say until your lang supports adding stacktrace to the errors, it becomes much harder to determine the reason for an error
You can use something like Snag to add context to your errors in Gleam, or Go has the ability to wrap errors. That provides enough context as long as you're diligent, I think. But yes, they don't come by default.
This is true for older languages like ocaml and haskell where the norm is just to use options, but result types with an error type and an ok type are better because the error variants come with info. These are more popular in rust and gleam.
An ("ugly") solution are what I call "meta-enums". The deepest function in the stack would return a typical `Result` where `E` is a custom enum. Then any fn that calls that fn must return a `Result` (defined as a custom `Result` enum, for readability) where `F` is `enum {self_err(F), inner_err(E)}`
„This pattern is very similar to the way the question mark operator works in Rust. But in Gleam, it’s just a function - no special syntax required. However, call-back based code like this can become quite nested. In the world of JS, this is often referred to as „callback hell“. […] Luckily for us, Gleam has a special syntax for flattening out callbacks.“ Do you want the special syntax now, or don’t you?
Fair point! The `use` syntax in Gleam is used for more than just error handling though, and it's completely optional. But yeah, I see where you're coming from :)
I thought it was funny that the video does not seem to recommend special syntax first, but then proceeds to use it. Either way, I like both. To be fair, the `?` Is optional as well and just syntactic sugar for a match expression. I‘m quite curious how Gleam compares to Rust. Seeing that it is built on top of Rust (nice), I would expect to be able to do all that Gleams aims to do with Rust as well; at least this is my initial impression. Maybe a video about Gleam vs Rust would be interesting too :)
Gleam is built using Rust, but they're fundamentally different languages. You're not the first person to compare the two though, so it's probably worth making some sort of video to clear that up 😅
@@IsaacHarrisHolt Can you use it with something like foreach? If so, it feels like a blatant violation of the least surprise principle, that after a certain point the code can be executed multiple (or 0 but early return is less shocking) times.
@@user-uf4lf2bp8t this is arguably still monadic, it’s just that you have to write the machinery again and again and again. I’m always shocked when I read Go code and every second statement is if err != nil { … }. Programming is about abstraction but for some reason the developers of the language seem intent on discouraging abstraction as much as possible. It’s always felt like a language designed for lines-of-code-written metrics, and the constant explicit error checking obscures the business logic. What’s particularly frustrating about Go is that it uses errors as values, in a way that is easy to get wrong and correct use is only enforced by convention. Compare Haskell’s Either, which is either an error or a value, never both, and you must pattern match on it before you can access the value. Go’s multiple return type and the “return some hopefully innocuous value along side the error because we don’t have sum types” approach is nuts to me.
I have worked extensively with systems that both use exceptions, and those which exclusively use error values. I have found that any sufficiently complicated system using error values eventually reinvents a less useful version of unchecked exceptions. Less useful because they typically do not provide a call stack, making debugging more difficult. The resulting code is also inherently more noisy, although mechanisms like Rust's question mark help to minimize this drawback.
I'm curious how you reinvent unchecked exceptions using error values. An unchecked exception is one you don't know can be thrown, but error values appear in the function signature. And I think too many people put emphasis on keeping code concise. I'd rather have to write a load of error handling code than not know where something is going to go wrong. And as for stack traces, this is fair, but you can provide context within an error.
@@IsaacHarrisHolt The problem is that a sufficiently complicated system will depend on many different libraries, each of which returns a Result with it's own error type. You can union these all together, but it becomes very messy. The next step is to map the error onto your own error type, but in doing so you lose some information from the error type, and you add even more noise to your code. The final step is that you either adopt or invent some generic error type that can wrap all other errors, now you have a unified error type for your entire application, but it's effectively just an unchecked exception, as the error type does not actually tell you any useful information. Snag is an example of this. If almost every function in your code returns snag.Result, then you aren't getting any information from the type. It's just an unchecked exception. The context is nice, but that's still basically just the stack trace of an exception. And almost every function will need to return a type like this, because in a sufficiently complex project almost every function can fail. The only functions that can't fail are those functions that are so trivial that you don't even need to look at them to know that they can't fail. I spent several years working on C++ code like this, because exceptions were forbidden in that codebase. And it was fine, our Result library was very good, we had macros that acted like Gleam's `use`, and we even implemented our own context like in Snag (this was long before Gleam or Snag existed, just convergent evolution). However after all that experience I never felt that it offered us anything that exceptions did not.
ditto. Also, panics. Forcing the caller to handle the error often results in passing the error up the stack as a return value because that wasn't the appropriate level to handle it. Exceptions are really good at what they do.
@@IsaacHarrisHolt The problem is that any sufficiently complicated system will depend on multiple libraries, each of which returns it's own error type. When you have a function that calls two different libraries, each of which can fail with different error types, you have to union those error types together. This quickly becomes unwieldy. So you use map error to map the library errors to your own error type, but in doing so you have lost the type information of the underlying error, and you have added more noise to your code. The next step is to use a library that wraps all error for you, but this is no more useful than unchecked exceptions. In a sufficiently complicated system, almost every function can fail. The only functions that can't are so trivial that you can tell just from the name that they don't fail. So you don't get much useful information from a function that returns a generic error type. Snag is an example of such a library. When every function returns `snag.Result(t)`, what information is this really telling you? The Snag contexts are nice, but they are basically just the same stack trace you would get from exceptions, except they can be skipped and produce more noise in the code, I spent a few years writing C++ code that exclusively used error values like this. It was fine, we had our own Result type, we had macros very similar to Gleam's `use` that made it easier to use, and we even added our own context support much like Snag (this was all years before Gleam or Snag existed, this is just convergent evolution). However after all this experience I never felt that the system offered any advantage over unchecked exceptions. When I write code using unchecked exceptions, I just assume that every non-trivial function call can fail, and this has never caused issues for me.
When you use errors as values you also get the advantage that the LSP of the language that you are using can help you Your LSP can't help you If the funcion that might throw an error has a try-catch inside: You have to leave your editor and go to the documentation for that function or (worse) take a look at the entire code + the code for the exception and so on, and all of that process is annoying, it breaks your flow
errors as values is so good once you use them that now every exception is painful to use/handle. literally thought of making a custom result type on work projects, but since i'd have to handle exceptions anyway it was not worth it... but yeah, the best thing is knowing what can fail and how instead of just fill everything with try-catch just in case its possible for a random unknown exception to appear.
We use a custom result type in our TS code at work, and it's sometimes a bit much, especially when there are other alternatives that better leverage the TS type system. But yeah, it's nice to know what's gonna break your code and be able to prepare for that.
every normal low-level language has type unions, calm down. exceptions are much better than values when you work in a REPL though, that's why even functional scripting languages use exceptions. as somebody who works with python (for data processing) i can tell that the trick about working with exceptions is not to catch them, unless you really need to, and not to throw them, unless the error is so big that it should reset execution to the most recent checkpoint (eg drop a record from a table, cancel/redo a task). then exceptions become quite manageable. but in standalone programs exceptions get out of hand very quickly indeed
I'm curious, what do you do in the case where you have a small error? Do you return it as a value? In which case, you're doing it the Gleam way already! You're effectively using exceptions to crash and reset your process, which is offensive programming and would be managed in Gleam using a supervision tree.
@@IsaacHarrisHolt depends on a case. may just log it (eg. when iterative algorithm does not converge, but there is still some sort of result). i also just log any unexpected exceptions when i batch-process a lot of files, so that a script won't stop because of one bad file. when error is very much expected (parsing, validating) may want to return a type union ( i usually use pair of int/enum + value, to indicate what value means, kind of like one would normally do type unions in C) then there are less common options: collect information about errors (input values, error messages) into array and return it / store in a global variable. this is just like logging, but you can easily use this data further down the line, eg select only those table records that give rise to a certain type of error. of course, this can be done with well-structured logging system, but list approach is much simpler. it is possible to feed errors to an impure error-handling function that does something complex. eg if you may want to have email/telegram notifications + save broken data separately, but this is more of a standalone program territory, than a repl session/script. however, a handler function may be useful in other scenarios in a repl context: i often use handler functions for plot post-processing (appying certain common adjustments, saving, deallocating) -- this way, i don't need to write same piece of code over and over again, but at the same time have several radically different behaviors, that i can switch on the fly by reassigning a global variable to a different function.
@@IsaacHarrisHolt depends on a case. may just log it (eg. when iterative algorithm does not converge, but there is still some sort of result). i also just log any unexpected exceptions when i batch-process a lot of files, so that a script won't stop because of one bad file. when error is very much expected (parsing, validating) may want to return a type union ( i usually use pair of int/enum + value, to indicate what value means, kind of like one would normally do type unions in C) then there are less common options: collect information about errors (input values, error messages) into array and return it / store in a global variable. this is just like logging, but you can easily use this data further down the line, eg select only those table records that give rise to a certain type of error. of course, this can be done with well-structured logging system, but the list approach is much simpler. it is possible to feed errors to an impure error-handling function that does something complex. eg if you may want to have email/telegram notifications + save broken data separately; this is more of a standalone program territory, than a REPL session/script. however, a handler function may be useful in other scenarios in a repl context: i often use handler functions for image/plot post-processing (applying certain common adjustments, saving, deallocating) -- this way, i don't need to write same piece of code over and over again, but at the same time can have several very different post-processors, that i can switch on the fly by reassigning a global variable to a different function.
It depends on the error. If you bubble an error up the stack, more and more tracing data will have to be stored and passed around. On the other hand, panics allow you to perform an autopsy on the stack without incurring any runtime penalties (because all panics are assumed to never occur, because if they do, it's lights-out for the program anyways).
Yes, I agree, but panics and exceptions aren't the same, which I think is something a lot of people seem to miss. And adding that context is what Snag is useful for
I firmly hold the opinion that the only cure for the problem is extensive testing that is as close to a production system as possible and logging as much information to reproduce issues if the come up. From my experience 99% Errors (be that errors as values or as exceptions) one of these 2 things is done when an error occurs: Either bubble it up, or do some implicit stuff that is specified exactly nowhere and probably not consistent to try and save the situation even if means just ignoring the error and continuing on as if nothing happened. From my experience the last option has lead to the most catastrophic outcomes and hardest to find bugs. Because instead of having the thing crash right when things go wrong they rear their ugly head on the other side of the planet where the wrong data has been mangled through and the issue might not be obvious.
Honestly. I like exceptions. My only issue with exceptions is they are implicit, where they should be explicit. Any function that can throw should be marked that it can throw.
@@IsaacHarrisHolt Yes, except you have clear separation of concerns, and you don't have to pollute codebase with sum type results when they should not be handled locally.
Personally, if I wanted to get errors as values....I'd use C. However, in my opinion, handling it that way gets to be rather annoying rather quickly. An exception in a language that supports a try catch like system can halt a borked function if it is raised. If you're just returning a value, you risk someone forgetting to add a check to see if the returned result is actually valid. Maybe you have a function that should return an offset for another part of your code, but for whatever reason, it encounters an error and returns -1. One simple oversight later, and now you've managed to not only crash prod, but you've corrupted several customer records in the process.
That's true when your error code is just an int, but in Gleam you can't access the value in the Ok without at least acknowledging that there could be an error
You can return Error objects instead of throwing them... It will give you roughly the same benefits in TypeScript. Just wrap 3rd party functions that throw and never throw yourself. Not as slick as in Gleam but much better than try/catch madness with half of code flows being untyped.
@@IvanKleshnin you can even check the error type and conditionally rethrow or handle it. Exceptions are excellent, people just don't spend effort understanding them.
I disagree from my (C++) perspective for at least 3 reasons: 1. If functions calls another functions and in the deep end you get an error, to propagate the error back now you need to check for error at each level. 2. If your function calls multiple functions you'll need to check for error after each call. This becomes very error prone when you need to release resources manually. 3. If you pass callback to 3rd party library (e.g. standard library) and callback returns error, there is no way for it to handle error to propagate it back.
I don't think that "having to" handle errors is a bad thing. It forces programmers to understand the error surface of their APIs and will ultimately promote a better understanding of their program as a whole. Also, I don't see how checking for errors is error prone? You're literally forced to deal with them. 3 can be an issue, I agree, but this is mostly solved by generics and good API design.
หลายเดือนก่อน
@@IsaacHarrisHolt imagine you check the current 3 types of errors that some call can throw. Tomorrow the API adds a fourth error and your code is not updated (because reasons).
If you're using a language with errors as values and exhaustive pattern matching, you can't get into this situation in the first place, because your code won't compile!
The problem isn't exceptions - it's that you can treat possibly-throwing code as safe code without any repercusions. Java did well with checked exceptions, but everyone hates them because they're annoying to deal with. What you need is a language that forces you to handle failures immediately in a convenient way, values or not.
@@IsaacHarrisHolt oh, yes. I do like Gleam's approach to error handling. I think errors as values is generally more solid than exception-throwing code. My point is that exceptions aren't as obsolete as it's somewhat painted in this video, but rather them being unwieldly is a result of little design effort put into them. For instance, there's a design article from one of Java's lead architects (inside.java/2023/12/15/switch-case-effect/) that proposes an addition to the switch's pattern matching capabilities to catch errors in a functional vein - similar to how effect handlers work. And this is no coincidence: FP's effects and Java's checked exceptions are equivalent constructs in these paradigms (FP vs imperative). The sudden ease in failure handling incentivizes Java users to add checked exceptions to their function signatures more widely, achieving the same success in terms of safety without errors as values.
I do not agree with the premise of this video. You know what else is just another word for goto? Return. Each exception doesnt create a new control flow, there is a flow for correct values (return) and a flow for errors (raise). All correct values are returned to the function call and all errors are raised to the try/except block. There are no other flows besides those two. Also, the "you no longer have to litter your code with try/except blocks" claim is technically true, just that you litter it with if statements and match cases instead. Finally, with exceptions I can simply write the only correct path for my code, and if any of the calls I make fails, If its well coded it will rise an exception and I can catch that from the caller. With errors as return values I must check each return value for errors at any step, whereas try/except allows me to handle all possible errors through the caller with a single try/catch statement. Try/catch statements are great because they can cover an entire function call, and I don't care where the function breaks, I can simply code the correct path and handle any errors later. Its a much more effective approach and its only confusing when people use error values in return in languages with try/except statements or when people dont declare which exceptions their code throws for each case, and return values as errors solve neither of those problems.
Writing try/except over your entire function is a terrible way to write code. You won't know which part of your function is breaking, meaning debugging will be ten times harder. Also, you don't know which functions are gonna throw, so you have to wrap EVERY function call with try/except unless you have full access to the source and the source of every function it calls. That's what I meant by the littering comment. At least with errors as values you only need to add extra handling when you KNOW there's going to be an error.
@@IsaacHarrisHolt In any language with stack traces or exception values you definitely know exactly where an error occurs. I've never seen anyone wrap every function in an exception handler, but I have seen nearly every function have a conditional that checks for error values. Any half-decent API doc generator should tell you if the a function throws exceptions, you don't need access to the source code.
Most API doc generators can't tell you if it throws because most languages don't have anything to mark that. The doc generator would have to crawl your source code and the source of every function you're calling. Also, yes, stack traces give you the location, but only AFTER the error has occurred. So you know what WENT wrong. Errors as values tell you every single thing that COULD go wrong.
@@IsaacHarrisHolt You won't write an try-catch over your function, if you intend to just keep it as error-condition. You just throw it up. Also you know which function throw an exception, because exceptions are part of the functions signature in most languages with exception-handling and the compiler will enforce that you handle the exception - but only if one is thrown in the code you call.
There are relatively few major languages where exceptions are part of the function signature. I know Java has the option of doing it, as does Swift, but iirc it's not always enforced. And in languages where you don't have it, your tooling would need to recursively check every function call down to the native layer to see all the kinds of exceptions that can be thrown.
the thing is that exceptions has always been the wrong mechanism. If you use condition-restart system of common lisp you get all the benefits of errors as values as well as the benefits of try-catch and some unique to conditon-restart system like unwind the stack, identify the mistake, fix the bug in the execution environment and then rerun the stack and finish the original process. You can also store the entire stack to do it later or send it to another environment.
It's a mistake to mix error handling with returning, no matter if returning Error values or returning multiple stack levels by an exception, because returning destroys the context in which an error might have been correctable. Have a look how the Common Lisp condition system works for a better approach.
I'm not familiar with it, but I think that a good error will contain the context required to debug it easily. I don't completely agree with your take here
Always argued with colleagues about their philosophy using try/catch and throwing random exceptions everywhere, instead of just returning an error-type, using a reference to "catch" the error in, or always returning a boolean to signal success of a method. All errors are expected and throwing UnexpectedExceptions is just a sign of a bad programmer. Fight me.
I don't want to fight you! But I'm not sure it's necessarily the sign of a bad programmer. A lot of it is down to the available tooling. Some languages don't have support for multiple return values, others don't have good pattern matching, etc. Errors as values work best when you have compilers and linters that can point out the fact you've missed an error, or where your FORCED to acknowledge the error value
Exactly. Throwing generic exceptions with no info is the same as a webservice returning 500s with no body. Useless and bad code. Imagine your returned error is always -1 and you're at the same spot.
I have to say, I think this whole discussion is borked. The video claims exceptions bad with stuff that is solved in some exception languages. Sure, if you compare with Javascript it will be bad. But other languages do exist, and they do stuff better. Similarly I could claim that errors as value are bad, because it is shit in C (the whole reason exceptions were invented is how bad it is). But I do not. Because I use both exceptions and errors as value. And I do that even in one code base. For instance in Java or Kotlin I can (and I did) build types that can contain an error. But I can also use exceptions beside them. The only problem here is, that the function decides in which way it relays errors. But in a error-as-values language I have difficulties to emulate exception when they are the better solution (and they can be). At least since Go the defer keyword exists, which solves at least one problem, as it brings the finally-block of exception-languages into error-as-value languages. Does Gleam support defer? Anyways, I wish for language support in which the caller could decide what way to use. It wouldn't even that difficult to begin with. The function declaration has to contain an indication if it can error. Then the caller can either call it in a way, say like this: val optional_result = ?example(); This syntax could automatically wrap the result into an union or option (or whatever the language has): either an error or the correct result. You could unwrap after checking for the error-state. Alternatively we could use an exception syntax: val result = !example(); In this case result already contains the unwrapped result but in the case of an error it goes to the next catch or if not present the function with this call has to declare to be able to error itself to bubble it up. That doesn't seem too complicated, why does nobody do it? Are people too much wrapped up in their thinking that one way of error handling excludes the other?
Firstly, sure, Java and Kotlin have something, but while you're forced to show that your function throws, are you forced to handle it? If you are, that's basically errors as values 😅 Gleam supports defer via `use`, if you write a defer function. The reason language creators don't implement both is because it would add complexity to programs, and errors as values are generally preferred these days. Sure, C got it wrong, but that doesn't mean the whole concept is bad. That said, the whole concept of exceptions? Pretty bad 😁
@@IsaacHarrisHolt Yes, if a function throws, you have to either handle it or bubble it up (by declaring the current function also throws). Well, what you say about C is my argument, only because JS has a terrible implementation that doesn't mean Exceptions are bad. And if you say that Java and Kotlin are basically like error-as-value you see that these concepts aren't as antithetic as you paint it. But you get back to claiming Exceptions are bad, based on nothing. And as you say, modern error-as-value has become a lot better since it adopts more and more ideas of exception handling and both get closer. So implementing both interchangeable as I described shouldn't be that difficult.
Java and Kotlin makes two languages, and it's still not a great approach. Java has unchecked exceptions like JS, which arguably makes things worse, as checked exceptions lull you into a false sense of security thinking you've handled everything when you might not have done.
How is it less complex than a try catch? You still have to handle the error cases. It has the same amount of logic behind it. In Java the functions that throw errors also have to be annotated, so you also see what error the functions throw in the same way as error return types.
True, but not every language is like Java. And complexity has nothing to do with how many lines of code you need to write, it's about being able to reason through your program. If I'm reading a Java function, and I see something that throws but isn't immediately handled, I have to look at all that function's callsites, and all those functions' callsites, etc. all the way up the call stack to figure out where it's going to get dealt with, or even if it's going to get dealt with at all.
Because it's genuinely a nice looking abstraction over explicit nested if else's. I'm not for bullshit language abstractions but this one is just naturally useful as it's not merely syntactic sugar. Under the hood its monads but the syntax makes it useful without you even knowing the theory behind monads. In Haskell it looks like this: data Maybe a = Nothing | Just a getDetails :: Name -> Maybe(Details) getDetails name = do personId
With a result you HAVE to handle it somehow. You also get better type safety, and useful functions you can use to manipulate them since they're just treated as values. Moreover, results are predictable. That's the most important thing.
@@IsaacHarrisHolt If we are comparing it with checked exceptions in java (which is what OC was talking about), then there's little difference. 1. "You HAVE to handle it" -> you can just propagate it up, you just have to be explicit about it (which yes, it means you are forced to handle it). But that's the exact same thing with (checked) exceptions, you either do something with it or you propagate it up by annotating the functions with the throwing type, otherwise you will have an error (i.e. you are forced to handle it) 2. "Better typing" -> disagree. With checked exceptions you can annotate the function with the exact type of exception, and you can have multiple ones at that (and you are forced to handle then all). With results you do the same just with different syntax, but I dont see how one is better than the other. 3. "Useful functions" -> this is completely language dependent (just like exceptions itself, where most languages chose not to have them represented in the type system, while Java, for all of its flaws does - with checked ones at least) 4. "moreover results are predictable" - in what ways? For functional style programming I can see results being easier to reason about, but for imperative languages what are the advantages? For the caller it's the same thing, for the callee it's not any more unpredictable than returns in the middle of the function. Results make more sense, imo, in functional languages, but for imperative languages I dont see any advantages of results over exceptions. The major difference is that with the latter you will usually have a callstack, but not with the former, so which one you use depends on the use case.
Errors with more info is good, and easily achieved with TryGet functions. Question: If a team of software engineers were to fully adopt Gleam, do they risk creating a product that could seemingly run like normal, but just not be coded properly to tell it is broken?
No more so than in any other language. With try/except, for example, you have the option of just not going anything with the error. It's the same with Gleam - you could just... not do anything when a process crashes or you get an error result. As usual, it's on the programmer to properly instrument their code and adhere to good observability practices
It's shocking to see so many commenters who don't get it, even after a good explanation. I'll add my 5 cents. With exceptions you have your SUCCESS logic paths typed, but ERROR paths remain untyped. Compilers do not enforce exception handling and, in JS especially, there's no typesafety with try/catch whatsoever. So if you're into static types - congratulations, but without errors as values you get at most 1/2 of the whole type safety. And if you're not into static types... my condolescences to your career.
Yes but even as someone who likes and regularly errors as types just blinding wrapping everything in a Maybe/Optionals or Result/Either is not the way to go. Especially when your project is sizeably large even in a language with the syntactic and compiler tools to handle complex errors like Haskell you may corner yourself into some unscalable and unrecoverable mess. Use a mix of crashing and type level error handling.
Yes, and just to be clear, you CAN crash. What's different is that, instead of resetting to a certain error boundary, you crash your process and let your supervisor start a new one
Case statements will force you to handle every variant. With try/catch and exception-style systems, you're never forced to even acknowledge the error exists. In Gleam and other error-as-value languages, you have to face the error down when it arises.
@@kered13 yes, in languages with typed and checked exceptions. Few of them exist, as you mentioned. But even such exceptions do not have any added value over Result type (throw is basically a goto if not immediately handled).
@@IvanKleshnin no. Gotos have a single target location, exceptions crawl back up the stack and they can be handled at ANY point along that crawl. JS is not strongly typed, yet exceptions do have types, just not strong (just like the rest of the language). also, you're supposed to throw useful information, and they definitely force you to acknowledge the error exists (unless you like topcrashes). If you write code that acknowledges the error (a catch) and then do nothing with that information.... well, you could do the same in Gleam and any others like it.
Like there were not enough null ptr problems, so founders have developed the concept of error so generation to come have proper suffering routine even when coding becomes easier.
Can you print a stack trace in gleam? In a debugging situation I would often like to log an entire stack trace for each error, so that I can fix my code.
It's not intended to be a scripting language, but you can run parts of modules if they have a main function. However, like Rust and other similar languages, you do need the project scaffold each time
All of this goes away if you learn to use exceptions properly :) Callback hell was solved in most languages years ago with the introduction of async (what a godsend, no more hadoukens!). I've been using exceptions for years in the (*gasp*) weakly typed world of JS without issues. No random crashing, I know exactly what happened and where and I can recover from it if it makes sense to do so. I always laugh out loud when I see a new exception-free language and then they explain panics.
It's not a case of "learning to use exceptions properly". Fundamentally, these are two different solutions to the same problem. The difference is that one gives you upfront visibility over what COULD go wrong, so you can deal with it while you're writing the code, and the other tells you what DID go wrong when your code has already crashed. Panics are meant for unrecoverable situations only. Exceptions can be and are used in both recoverable Andy recoverable situations, making it difficult to discern and differentiate.
What, can other programming languages not quit to desktop?? Also, what happens if someone used this code on top of one that just decided to catch whatever the new language tried and roll through?
Without going all the way to errors as values, I always found Java's checked exceptions to be really useful, something sorely missing in other languages. I see arguments against them, but I never really agreed and don't see how errors as values don't have exactly the same downsides. Furthermore, strict null checking in Typescript has been really useful to me. Null is another value that can slip through unnoticed too easily in many languages. Option types work too in other languages, as long as you are forced to handle them.
Pretty much always use TS instead of JS now for this exact reason. I also like how TS sort of lets you get away with enum types using a type field. That is, you can do something like type Result = {type: "ok", value: T} | {type: "err", value: E} Now in any code using such a Result, access to value is guarded, but in any if statements that check something like if (r.type == "ok"), it will let you use the corresponding type.
Both good solutions! I'm not familiar with Java, but we use this style of error in TS at work, and it's pretty great. The problem is one of the functions you're calling in a library might throw, and you'll never know until it does. On the null and option side, Gleam has both of those things! Nil is a unique type that can't be assigned to anything else (like () in Rust) and you have the Option type for values that may or may not be present
The problem with checked exceptions like in Java is that it doesn't work well with callbacks. This became a significant problem when Java introduced the Stream API. You can't pass any function that can throw a checked exception into stream map, filter, etc. without using some hacky workarounds. This problem can be solved with a sufficiently strong polymorphic type system, like C++ for example, but very few languages have that.
Im using vanilla HTML/JS for a project right now and I just want my dear sweet errors as values back. Gleam looks great, i might try it for my next project
Error handles must be able to support also errors like out of memory or stack overflow. Those errors d not work as 'optionals/eithers'. Also, what if you get an error while 'writing'' a file instead of 'reading' one? now you get a result Either but.... you are not going to look into that result anyway, since you wanted to 'write' you are unlikely to care about the result, and now the error is effectivelly suppressed. Not very offensive style.
In the case of a memory or stack overflow, your process will probably just crash and be restarted by a supervisor. And for writing files, that's fine. You're still allowed to choose to ignore errors but at least you know they're there and may crop up.
Thanks for answering, I hope we can have a constructive chat. @@IsaacHarrisHolt 'you are still allowed to choose to ignore errors' Sure, this is what we did with old C error code as returns of functions.. and everyone ignored them. Note that I'm not pushing for standard exceptions, I'm just saying that optionals/eithers are a step back, and that we need some way to make sure that errors are explicitly handled in one way or another at all times, not just when we care about the operation result. - Depending on how 'crash and be restarted' works, .... it looks a lot like a try-catch. - Offensive programming is really the right style in my opinion, but it makes perfect sense to do it in layers. Where logically you have a few layers of master/slave patterns, where the slave computation can die, and the master computation can decide what do to, somehow protected by any corrupted state that the slave computation left behind. If 'crash and be restarted' works with actual processes, well.. that is too limited and relies too much on the OS, in my opinion.
In this case I'm referring to BEAM processes, which aren't tied to the OS. Erlang has the concept of supervisors which are essentially processes that look after other processes (I have a video on this if you're interested). As for ignoring returned errors, sure, you could do that with C etc., but in C, when you DID want to look at the error, it was a lot more difficult to figure out what actually went wrong. You'd be relying on the author of the function documenting what int means what in some way, and different authors would have different standards for that. Gleam makes this trivial by allowing tagged unions types as the error type, so you easily get the type of error and any context attached to it.
@@IsaacHarrisHolt Sorry, it's not quite relevant to your video, other than the idea of eschewing exceptions in favour of error values. I spend most of my time writing C++, and in that context, I see people recommending such a coding style, but I've never been given a satisfactory answer as to how to do that in that language without it becoming extremely verbose. The reason is that for my purposes, I want to write code that goes into a shared library that gets used by external programs. In that context, I never want to crash or corrupt the process (except for doing the former if I detect the latter), so that means I _do_ want to handle out-of-memory conditions (i.e. `new SomethingImpl()` throwing std::bad_alloc or `new (std::nothrow) SomethingImpl()` returning nullptr) by gracefully unwinding the stack, and then returning an error code via the C API that the shared library is called via by the host program, not simply assume that it can never happen, or just calling exit(1) or abort() or something, which is what I usually hear suggested. The problem is that almost EVERYTHING other than 'mathy' functions probably needs to allocate internally to do its job, at least sometimes, and that allocation _can_ fail, so it makes almost EVERY return value an std::optional or something equivalent. By writing my code using exceptions for error handling, and 'simply' assuming that ANYTHING (other than a few well-known things which are _documented_ to not throw) can throw, and using proper RAII everywhere possible and a 'transactional' style, I find that doing this is relatively straightforward without polluting too much of my code.
Ive been using return values as errors since veny long. Its not new, i think this was the norm long time ago. I do it In a sort of 'defensive programming' way. It doesnt depend on any special language feature. And i do things a bit differently than you. Im quite proficient with exceptions, but i like my way better. And i think it works really well. I make games with unreal, and (besides exceptions not being available) a critical error can easily crash the game. With my approach the crashes are only due to very low level stuff on the engine. And i usually patch those cases.
Yes, errors as values have been around for a long time. That said, certain type systems make it easier. For example, C errors are typically just ints, meaning you have to then use some sort of lookup table to figure out exactly what went wrong in any meaningful way.
So instead of a try catch block catching explicit errors, use "use" for that block's length plus a switch or whatever later on. Seems the same with extra steps.
The difference is that try/catch is pretty much always optional. At least with `use` you're forced to write it in, acknowledging there's an error possibility.
@@IsaacHarrisHolt While custom ECC and triple redundancy to withstand cosmic rays might land me a job at ESA, i just want to parse some JSON. A crash is fine. Does Gleam even have a formal prover like Spark Ada though?
I never understood the reason to catch errors. If there is an error in the system, the code shouldn't catch it and handle it in some weird workaround ways. The code should be fixed so that the error cannot occur anymore. When is a try catch actually truly useful and not due to bad code?
There are always things that can go wrong that are outside of your control. You might be trying to open a file that doesn't exist, or your internet connection might drop out in the middle of a HTTP request, or a million other things. You can't prevent every error
Exceptions are for code that cannot be handled locally. If you encounter out of memory error, you don't want to handle it in caller functions. You want to unwind stack until handling lack of memory makes sense.
Seems like older C/C++ API's got it right the first time. We use a lot of async boost in our server application and we just default to the error_code version of the methods unless we specifically need to handle exceptions. Its much easier to reason about an error code, especially in callback based code.
3:35 Bubbling the error is very common in functional languages. Most (if not every?) of the error types are monads, which means that you can define a way to compose functions on those types, and if an error value occurs, carry it to when it's matched on. Edit: Ok it's described on 5:04, nice ;)
@@IsaacHarrisHolt It works differently under the hood, but the result is very similar - a nested function can return an error but you only have to match at the very top, just like you would do with `try catch`.
Of course, but with errors as values you have to explicitly pass it back up the call stack. With exceptions it just kinda... appears at the closest try/catch that matches it
You're still passing it explicitly. `use` is just a language feature like Rust's `?`. You're still making the choice to say "I have this error but don't want to deal with it, so the caller can have it now". It's still explicit
Using errors as values instead of exceptions feels like trying to reinvent the wheel, but using triangles instead. 🤷♂️ Exception handling is crucial for creating robust and maintainable applications and libraries, particularly in environments that have evolved over many years or decades. In such environments, development errors are more likely, especially when incorporating dynamically linked third-party code at runtime. Exceptions provide a structured and standardized way to handle unforeseen situations that can arise in these complex systems. While using errors as values can be appropriate in specific scenarios, such as in performance-critical code that has been thoroughly tested and is unlikely to encounter unexpected issues, it is not a one-size-fits-all solution. Exceptions generally offer a more reliable and maintainable approach for the majority of applications, enabling better error propagation and handling.
I agree with some points here, but I disagree with exceptions handling being more readable. If I have to go function hunting to find where a particular exception is coming from, I'm having a bad time. Also, in most languages, you lose type safety when dealing with exceptions, which negatively impacts your ability to correctly identify and handle errors as they arise, leaving you playing catch up while your software's in production.
@@IsaacHarrisHolt That is mostly true, but what I wanted to say was that a hybrid approach will get the desired result. Combine exceptions and error return types for robust error handling. Use exceptions for critical infrastructure and initialization errors that require halting the system, ensuring immediate resolution and preventing instability. Employ error return types for business logic and non-critical errors, explicitly managing and propagating them to enhance type safety and predictability. This hybrid approach balances the concise control flow of exceptions with the explicit error management of return types, optimizing performance and reducing complexity. Clearly define boundaries for each method, document guidelines, ensure consistent handling, and implement thorough testing. This strategy supports maintainability, type safety, and efficient error management in complex systems.
That's basically what Gleam does with a panic. It's not an exception, because you can't catch it, so it's only used to stop the program. I wouldn't say that's a hybrid model.
@@IsaacHarrisHolt I don't see how the erros as values are solving the "function hunting" problem. You say in the video that most of the time, you'll just be propagating errors up to the caller, which I agree with. But then, a few layers up, when you actually handle the error - how do you know where it originated? You have to go function hunting as well, no? Maybe it'll be a specific type that you can associate with a particular function. But then, if you call multiple different functions, how do you propagate all of their different error types upwards? There would have to be some common error type which can hold all of the specific functions error types. And then we're back at the question: how do you find the function this error came from? This weakens the type safety argument as well. With errors as values, if a function wants to propagate errors up, it'll need to use a common type for all errors that could occur within it. In many exception-based languages there is a common base class/interface that's enforced for all exceptions. Is there really such a big difference in type safety?
There's a huge difference in type safety. It's unlikely that you'll just have a single error type that everything uses - if you look at the stdlib, you'll see that the functions there generally have their own special error types, and that's generally encouraged. I see your point about the function hunting argument, but you also know that the functions above will HAVE to handle the error in some way, even if that is just returning it up the call stack. With exceptions you don't get that guarantee. When you're writing a function that throws an exception, you're making the assumption that everyone who then calls that function will handle the exception somewhere and it won't crash the program. With errors as values, you're FORCING callers to handle the error somewhere.
We are back in the same spot in history where we have error codes like in C++ era. But honestly, Error enums sounds like the best of both worlds. I always hated try-catches.
5:05 "its just a function, no special syntax required" 5:19 "luckily for us, gleam has a special syntax" edit: @ 5:50 this is literally just (imo) uglier ? syntax. ill stick with a singular way of declaring variables
Why is this video tagged as python lol? Exceptions are special values on certain languages. A lot of the tactics you use are pretty similar to how exceptions. I think the satement that youre saved from unhandled errors is not accurate.
Imo, exceptions are my main pain with C++. The langage spec is convoluted and over complex due to the fact that it's a systems language from the 80s that needs backwards compatibility with C and its past self? Sure. Does it have 5 ways of doing almost exactly the same thing? Sure. My main problem is that error handling is awful. You have no real way of knowing if a function will throw or not (most functions can't be nothrow due to memeory allocation, and most people don't mark it anyways). That opens so many possibilities of forgetting to check an exception and it randomly crashing your program. It's really annoying, especially since it's pretty nuch the only way to error handle from a constructor (yes factory methods are a thing, they're not widespread and have issues)
I think try-catch was born to pay more attention to the code that can crash everything, in pure C many call functions without managing the return value, the compiler will use a temporary variable and will not give any error at compilation time, the program then crashes and nobody knows why. C++ try-catch should help to pay more attention to the code and specify the error better, in addition to a maintenance issue: you can use ctrl+F to search for "try" instead of having to create a common standard for error variables.
I don't think try/catch works, though. Sure you can search for where it's happening, but that's not helpful if you don't know where errors are coming from.
So, do you mean the widely known and traditional C-way? int err = getaddrinfo(SERVER_HOSTNAME, SERVER_PORT, &hints, &result); Not sure why the need to present it in a light of a new language
There isn't necessarily code for everything I say, but I'm trying to reduce the number of GIFs. That does mean the videos take longer to make, though 😅
@@IsaacHarrisHolt Sorry. I am being a drag. I enjoyed the video otherwise! You're good! It's just that these things get old so fast. Life as a meme is short and brutish. Then again, maybe someone else would love these things. You can't please everyone all the time. I don't know. Just do what you love and serial moaners like me can go fork themselves with rusty kitchenware!
Some get really distracted with stuff like those gifs. Im one of those. I end up not even watching the video. I rather see nothing if theres nothing that adds information that i need to watch.
You literally use monoids every day, we all do, (numbers,+,0), (numbers, \*, 1), (lists, append, []), (bool, &&, true), (IO, >>=, return) _(ok that last one may be a little controversial)_
@@IsaacHarrisHolt True. Still, Go's and other languages and paradigms way of handling errors by returning error values is a lot like how C does it, it even seems like Go was inspired by C in that way. So, moving away from exceptions and back to handling errors as values isn't really something new, it's just getting back to basics.
Yes absolutely. The industry is finally realising that we were nearly there all along. Gleam is nice because you have the enums, though, which helps a tonne
I don't like how you present all of this in a way to only ever mention Gleam, and not any other programming language. Especially at 2:35 - the "other language" is literally the language Gleam's compiler is written in. Yet, you go out of your way to avoid ever mentioning it, despite most of the features presented being literally copy-pasted from it. This is dishonest.
@@IsaacHarrisHolt yes you are, but a) this video is advertised without any indication that it is Gleam specific, and b) while I understand not wanting to repeat "like in Rust" in almost every section, not saying it what-so-ever, or substituting it with "other language" is even worse.
But it's not just Rust. I said "modern programming languages", and showed Go, Zig, Rust and Elixir. I don't specifically call out Rust or Rust-specific features at any point. I believe I mention that you might be familiar with the `Result` type from other languages, but it's not like Rust invented that type or that idea - it's been common in functional languages for decades. Do I have to call out EVERY language that has ever used a `Result` type to encapsulate errors? Also, fair on the packaging point, but you're probably seeing the 1 thumbnail variation of 3 that doesn't include the Gleam logo, so I've now got some extra feedback on that experiment. Thanks 🙂
@@IsaacHarrisHolt to be clear, I wasn't arguing about the "modern programming languages" part, although now that you mentioned it - I didn't really read that code on screen, and for sure didn't recognize it as four different languages. Putting their logos near the respective code snippets would have been great. As for the "Result" type, I repeatedly hear "from other language" in singular, although without a particle. So... My bad for assuming the worst, I guess. And I really cannot hear the plural "-s" there, no matter the volume. I guess the argument is resolved now, sorry for throwing accusations (I repeatedly can't remember not to do that, when I'm right about to do that).
We have successfully returned to ideas from the C programming language and decorated them as something new once again! To be fair, while the idea isn't exactly new, gleam definitely executes the idea better then C does in regards to ease of development.
Actually, there are a lot of langs that used `Result` before just Rust, so it wouldn't have been great to name just one. I would have had to name all of them, which might've taken a while 😅
They have some overlap (e.g API development), but you'll have a much better time building a distributed, multi-threaded system in Gleam, since that's what the BEAM is built for. Similarly, you'd probably get along better with Rust if you're writing an operating system or something low level. The languages have different design goals
I honestly don't like this kind of videos. Don't tell me what to do, if throwing errors works, then it works. I won't stop doing what I know works (and works well at that)
@@IsaacHarrisHolt Have you read the README for the same gleam_otp package you mention? «This library does not currently replicate all of the Erlang/OTP functionality». Just use elixir. It's alive and well. "Type safety" cancer is not what is needed for distributed systems. Joe Armstrong would've sighed with disappointment if shown this idiotic language with no point whatsoever, with no contribution whatsoever to the BEAM ecosystem or the distributed programming experience. It's not even a lisp. Cringe.
Of course gleam_otp doesn't have feature parity with Erlang. It's a much younger language. Thinking it's going to be exactly the same is naive. And on your point about typing, yes it introduces some challenges, but it also fixes a whole host of issues you would otherwise see in your programs, distributed or not. I would implore you to actually try Gleam for a reasonably-sized project and see how you like it. Badmouthing something without trying it is close-minded.
@@IsaacHarrisHolt I have erlang and elixir experience, and I tried gleam. It's nothing new. Elixir fixes many aches with erlang, including type aches if there are some in your case. And it has macros. What is the availability status of modules that contain behaviours and macros from other beam langs? Does gleam still need the same fat layer of glue code to fix its inherent incompatibility with libraries that contain too much "let it crash"?
"throw is just a fancy goto". I mean yes, but all control flow mechanisms are just fancy goto.
True, but most are predictable. Throw could take you anywhere
Gleam doesn't have those, other than panic. No if else, no loops, no early returns, no exceptions.
@@user-uf4lf2bp8t I'm pretty sure pattern matches and function calls both have gotos in them if you strip off enough layers of abstraction
To be clear, i think the abstractions are good and was mostly just being silly saying everything is a goto.
@@IsaacHarrisHolt Throw could take you anywhere in the same way return could take you anywhere. In reality, throw works like a super return, it is only going to take you up the call stack, not to some random place in the code that has a try/catch. Saying it can take you 'anywhere' always makes me assume the person has never actually worked with exceptions.
Yes, it can only take you up the call stack, but unless you have visibility on the whole call stack, you can't see where that error is going to pop out.
Also, you don't KNOW which functions are going to throw, which is the whole problem.
Also, people forget that errors as values is something that it was used in C originally and people invented the try/catch because people got tired of handling errors as values.
so is just the pendulum is swinging back to the other side.
Exactly. People just easily forget the past. We once were very happy with simple errors as values returned from functions, and this comes from even before C, it was just the simplest, most natural way of doing things considering how computers actually operate with data under the hood... yet for some reason someone decided that we needed exceptions. Now, we're coming back to errors as values, but, yet again, just like with exceptions, there seems to be a need to overcomplicate the system when you can just return a simple integer value for the error... the pendulum is indeed swinging back, but with each swing, it appears that the artificial complexity obtained in one swing is carried over to the next. Let's see when we finally come back to simple integer return types for errors and no structs with Ok and Err return types or anything like that.
@@tiranito2834I personally think it's good we're not using integers anymore since enums makes the code more readable and less hacky.
With integers you can introduce infinitely many variants without a way to track them without a convention in place, whereas enums enforce keeping all variants within a single declaration, so that's a win for readability.
@@mysterry2000wait til you find out that integers and enums are interchangeable in C
I'm on the enum side of this argument. Even if they're functionally the same thing, having a named, pre-defined set of values your function can return is infinitely better than just returning "some number", and not every C program follows the same standards. Argon2, for example, returns -1 through -36 ish for their errors, whereas other libs may use positive ints.
Yeah enums today and strong typing offer a much more ergonomic experience than magical error codes in C. Even if they are interchangeable
The only thing that exceptions have over errors are values is the fact they always come with a call stack. When something goes wrong, you know exactly which file to open and in which line to look, or at least where to put a breakpoint.
I would say until your lang supports adding stacktrace to the errors, it becomes much harder to determine the reason for an error
You can use something like Snag to add context to your errors in Gleam, or Go has the ability to wrap errors. That provides enough context as long as you're diligent, I think. But yes, they don't come by default.
This is true for older languages like ocaml and haskell where the norm is just to use options, but result types with an error type and an ok type are better because the error variants come with info. These are more popular in rust and gleam.
@@user-uf4lf2bp8tlol Haskell already uses the original form of result type, it's called Either.
data Either err value = Left err | Right value
@@IsaacHarrisHolt The problem is that programmers are not diligent.
An ("ugly") solution are what I call "meta-enums". The deepest function in the stack would return a typical `Result` where `E` is a custom enum. Then any fn that calls that fn must return a `Result` (defined as a custom `Result` enum, for readability) where `F` is `enum {self_err(F), inner_err(E)}`
Tom truly is a genius. We're all quiche-eaters when compared to him
At least quiche is tasty
Tom is a genius
@@opposite342 iykykaik
JDSL > Gleam
😂😂
„This pattern is very similar to the way the question mark operator works in Rust. But in Gleam, it’s just a function - no special syntax required.
However, call-back based code like this can become quite nested. In the world of JS, this is often referred to as „callback hell“. […] Luckily for us, Gleam has a special syntax for flattening out callbacks.“
Do you want the special syntax now, or don’t you?
Fair point! The `use` syntax in Gleam is used for more than just error handling though, and it's completely optional. But yeah, I see where you're coming from :)
I thought it was funny that the video does not seem to recommend special syntax first, but then proceeds to use it. Either way, I like both.
To be fair, the `?` Is optional as well and just syntactic sugar for a match expression.
I‘m quite curious how Gleam compares to Rust. Seeing that it is built on top of Rust (nice), I would expect to be able to do all that Gleams aims to do with Rust as well; at least this is my initial impression. Maybe a video about Gleam vs Rust would be interesting too :)
Gleam is built using Rust, but they're fundamentally different languages. You're not the first person to compare the two though, so it's probably worth making some sort of video to clear that up 😅
and this syntax is 'async'
sorry I meant '?'
@@IsaacHarrisHolt Can you use it with something like foreach? If so, it feels like a blatant violation of the least surprise principle, that after a certain point the code can be executed multiple (or 0 but early return is less shocking) times.
My code never breaks. It just has happy little features.
Happy little unplanned features? 👀
Isn't this just plain monadic computation, that's being used in functional programming since... forever?
Yep, but the rest of the industry has finally realised it's a good thing
But they will still all complain when you use the word monad and claim it’s too difficult to understand.
100%
Errors as values isn't always monadic. In gleam it is, but go just returns 2 values and you check if the error value is not null.
@@user-uf4lf2bp8t this is arguably still monadic, it’s just that you have to write the machinery again and again and again. I’m always shocked when I read Go code and every second statement is if err != nil { … }. Programming is about abstraction but for some reason the developers of the language seem intent on discouraging abstraction as much as possible. It’s always felt like a language designed for lines-of-code-written metrics, and the constant explicit error checking obscures the business logic.
What’s particularly frustrating about Go is that it uses errors as values, in a way that is easy to get wrong and correct use is only enforced by convention. Compare Haskell’s Either, which is either an error or a value, never both, and you must pattern match on it before you can access the value. Go’s multiple return type and the “return some hopefully innocuous value along side the error because we don’t have sum types” approach is nuts to me.
I have worked extensively with systems that both use exceptions, and those which exclusively use error values. I have found that any sufficiently complicated system using error values eventually reinvents a less useful version of unchecked exceptions. Less useful because they typically do not provide a call stack, making debugging more difficult. The resulting code is also inherently more noisy, although mechanisms like Rust's question mark help to minimize this drawback.
I'm curious how you reinvent unchecked exceptions using error values. An unchecked exception is one you don't know can be thrown, but error values appear in the function signature.
And I think too many people put emphasis on keeping code concise. I'd rather have to write a load of error handling code than not know where something is going to go wrong.
And as for stack traces, this is fair, but you can provide context within an error.
@@IsaacHarrisHolt The problem is that a sufficiently complicated system will depend on many different libraries, each of which returns a Result with it's own error type. You can union these all together, but it becomes very messy. The next step is to map the error onto your own error type, but in doing so you lose some information from the error type, and you add even more noise to your code. The final step is that you either adopt or invent some generic error type that can wrap all other errors, now you have a unified error type for your entire application, but it's effectively just an unchecked exception, as the error type does not actually tell you any useful information.
Snag is an example of this. If almost every function in your code returns snag.Result, then you aren't getting any information from the type. It's just an unchecked exception. The context is nice, but that's still basically just the stack trace of an exception.
And almost every function will need to return a type like this, because in a sufficiently complex project almost every function can fail. The only functions that can't fail are those functions that are so trivial that you don't even need to look at them to know that they can't fail.
I spent several years working on C++ code like this, because exceptions were forbidden in that codebase. And it was fine, our Result library was very good, we had macros that acted like Gleam's `use`, and we even implemented our own context like in Snag (this was long before Gleam or Snag existed, just convergent evolution). However after all that experience I never felt that it offered us anything that exceptions did not.
ditto. Also, panics.
Forcing the caller to handle the error often results in passing the error up the stack as a return value because that wasn't the appropriate level to handle it.
Exceptions are really good at what they do.
@@IsaacHarrisHolt The problem is that any sufficiently complicated system will depend on multiple libraries, each of which returns it's own error type. When you have a function that calls two different libraries, each of which can fail with different error types, you have to union those error types together. This quickly becomes unwieldy. So you use map error to map the library errors to your own error type, but in doing so you have lost the type information of the underlying error, and you have added more noise to your code. The next step is to use a library that wraps all error for you, but this is no more useful than unchecked exceptions. In a sufficiently complicated system, almost every function can fail. The only functions that can't are so trivial that you can tell just from the name that they don't fail. So you don't get much useful information from a function that returns a generic error type.
Snag is an example of such a library. When every function returns `snag.Result(t)`, what information is this really telling you? The Snag contexts are nice, but they are basically just the same stack trace you would get from exceptions, except they can be skipped and produce more noise in the code,
I spent a few years writing C++ code that exclusively used error values like this. It was fine, we had our own Result type, we had macros very similar to Gleam's `use` that made it easier to use, and we even added our own context support much like Snag (this was all years before Gleam or Snag existed, this is just convergent evolution). However after all this experience I never felt that the system offered any advantage over unchecked exceptions. When I write code using unchecked exceptions, I just assume that every non-trivial function call can fail, and this has never caused issues for me.
When you use errors as values you also get the advantage that the LSP of the language that you are using can help you
Your LSP can't help you If the funcion that might throw an error has a try-catch inside: You have to leave your editor and go to the documentation for that function or (worse) take a look at the entire code + the code for the exception and so on, and all of that process is annoying, it breaks your flow
Great point!
errors as values is so good once you use them that now every exception is painful to use/handle.
literally thought of making a custom result type on work projects, but since i'd have to handle exceptions anyway it was not worth it...
but yeah, the best thing is knowing what can fail and how instead of just fill everything with try-catch just in case its possible for a random unknown exception to appear.
We use a custom result type in our TS code at work, and it's sometimes a bit much, especially when there are other alternatives that better leverage the TS type system.
But yeah, it's nice to know what's gonna break your code and be able to prepare for that.
WOW, IT’S ALMOST LIKE HASKELL HAD THIS RIGHT THIRTY YEARS AGO. *cough* sorry, had something stuck in my throat.
Never said it didn't! But the rest of the industry is finally catching up
every normal low-level language has type unions, calm down.
exceptions are much better than values when you work in a REPL though, that's why even functional scripting languages use exceptions. as somebody who works with python (for data processing) i can tell that the trick about working with exceptions is not to catch them, unless you really need to, and not to throw them, unless the error is so big that it should reset execution to the most recent checkpoint (eg drop a record from a table, cancel/redo a task). then exceptions become quite manageable.
but in standalone programs exceptions get out of hand very quickly indeed
I'm curious, what do you do in the case where you have a small error? Do you return it as a value? In which case, you're doing it the Gleam way already! You're effectively using exceptions to crash and reset your process, which is offensive programming and would be managed in Gleam using a supervision tree.
@@IsaacHarrisHolt depends on a case.
may just log it (eg. when iterative algorithm does not converge, but there is still some sort of result). i also just log any unexpected exceptions when i batch-process a lot of files, so that a script won't stop because of one bad file.
when error is very much expected (parsing, validating) may want to return a type union ( i usually use pair of int/enum + value, to indicate what value means, kind of like one would normally do type unions in C)
then there are less common options:
collect information about errors (input values, error messages) into array and return it / store in a global variable. this is just like logging, but you can easily use this data further down the line, eg select only those table records that give rise to a certain type of error. of course, this can be done with well-structured logging system, but list approach is much simpler.
it is possible to feed errors to an impure error-handling function that does something complex. eg if you may want to have email/telegram notifications + save broken data separately, but this is more of a standalone program territory, than a repl session/script. however, a handler function may be useful in other scenarios in a repl context: i often use handler functions for plot post-processing (appying certain common adjustments, saving, deallocating) -- this way, i don't need to write same piece of code over and over again, but at the same time have several radically different behaviors, that i can switch on the fly by reassigning a global variable to a different function.
@@IsaacHarrisHolt depends on a case.
may just log it (eg. when iterative algorithm does not converge, but there is still some sort of result). i also just log any unexpected exceptions when i batch-process a lot of files, so that a script won't stop because of one bad file.
when error is very much expected (parsing, validating) may want to return a type union ( i usually use pair of int/enum + value, to indicate what value means, kind of like one would normally do type unions in C)
then there are less common options:
collect information about errors (input values, error messages) into array and return it / store in a global variable. this is just like logging, but you can easily use this data further down the line, eg select only those table records that give rise to a certain type of error. of course, this can be done with well-structured logging system, but the list approach is much simpler.
it is possible to feed errors to an impure error-handling function that does something complex. eg if you may want to have email/telegram notifications + save broken data separately; this is more of a standalone program territory, than a REPL session/script. however, a handler function may be useful in other scenarios in a repl context: i often use handler functions for image/plot post-processing (applying certain common adjustments, saving, deallocating) -- this way, i don't need to write same piece of code over and over again, but at the same time can have several very different post-processors, that i can switch on the fly by reassigning a global variable to a different function.
It depends on the error. If you bubble an error up the stack, more and more tracing data will have to be stored and passed around.
On the other hand, panics allow you to perform an autopsy on the stack without incurring any runtime penalties (because all panics are assumed to never occur, because if they do, it's lights-out for the program anyways).
Yes, I agree, but panics and exceptions aren't the same, which I think is something a lot of people seem to miss. And adding that context is what Snag is useful for
As a gopher, I never want to use a language that doesn’t have errors as values
Great take. Go is also great!
This is true, I am also a Gopher
Gophers rise up!
As a rustacean I completely agree
exceptions no no no
panics ok!
(it's the same)
I firmly hold the opinion that the only cure for the problem is extensive testing that is as close to a production system as possible and logging as much information to reproduce issues if the come up. From my experience 99% Errors (be that errors as values or as exceptions) one of these 2 things is done when an error occurs: Either bubble it up, or do some implicit stuff that is specified exactly nowhere and probably not consistent to try and save the situation even if means just ignoring the error and continuing on as if nothing happened.
From my experience the last option has lead to the most catastrophic outcomes and hardest to find bugs. Because instead of having the thing crash right when things go wrong they rear their ugly head on the other side of the planet where the wrong data has been mangled through and the issue might not be obvious.
This is fair! I definitely agree that thorough testing solves a lot of issues. And having reasonable processes, too.
Honestly. I like exceptions.
My only issue with exceptions is they are implicit, where they should be explicit.
Any function that can throw should be marked that it can throw.
That's basically just errors as values then, especially if you're then forced to handle the exception
@@IsaacHarrisHolt Yes, except you have clear separation of concerns, and you don't have to pollute codebase with sum type results when they should not be handled locally.
I take it as everything can throw, which is usually the case
Personally, if I wanted to get errors as values....I'd use C. However, in my opinion, handling it that way gets to be rather annoying rather quickly.
An exception in a language that supports a try catch like system can halt a borked function if it is raised. If you're just returning a value, you risk someone forgetting to add a check to see if the returned result is actually valid.
Maybe you have a function that should return an offset for another part of your code, but for whatever reason, it encounters an error and returns -1. One simple oversight later, and now you've managed to not only crash prod, but you've corrupted several customer records in the process.
That's true when your error code is just an int, but in Gleam you can't access the value in the Ok without at least acknowledging that there could be an error
@@IsaacHarrisHolt same in exceptions.... and if you can't handle at the caller level, you let it pass so the next level up can handle it.
Coming from JS world, errors as values is such a good feature🙌
Agreed! Exceptions are painful
You can return Error objects instead of throwing them... It will give you roughly the same benefits in TypeScript. Just wrap 3rd party functions that throw and never throw yourself. Not as slick as in Gleam but much better than try/catch madness with half of code flows being untyped.
@@IvanKleshnin you can even check the error type and conditionally rethrow or handle it. Exceptions are excellent, people just don't spend effort understanding them.
@@MadsterV that's funny. Good luck with programming.
I disagree from my (C++) perspective for at least 3 reasons:
1. If functions calls another functions and in the deep end you get an error, to propagate the error back now you need to check for error at each level.
2. If your function calls multiple functions you'll need to check for error after each call. This becomes very error prone when you need to release resources manually.
3. If you pass callback to 3rd party library (e.g. standard library) and callback returns error, there is no way for it to handle error to propagate it back.
I don't think that "having to" handle errors is a bad thing. It forces programmers to understand the error surface of their APIs and will ultimately promote a better understanding of their program as a whole.
Also, I don't see how checking for errors is error prone? You're literally forced to deal with them.
3 can be an issue, I agree, but this is mostly solved by generics and good API design.
@@IsaacHarrisHolt imagine you check the current 3 types of errors that some call can throw. Tomorrow the API adds a fourth error and your code is not updated (because reasons).
If you're using a language with errors as values and exhaustive pattern matching, you can't get into this situation in the first place, because your code won't compile!
@@IsaacHarrisHolt If you're code is linked dynamically, which is most of the time, it can happen.
You don't link code dynamically in Gleam :)
The problem isn't exceptions - it's that you can treat possibly-throwing code as safe code without any repercusions. Java did well with checked exceptions, but everyone hates them because they're annoying to deal with.
What you need is a language that forces you to handle failures immediately in a convenient way, values or not.
I would say that Gleam is that - you can't access the result of the computation without at least acknowledging that there's an error
@@IsaacHarrisHolt oh, yes. I do like Gleam's approach to error handling. I think errors as values is generally more solid than exception-throwing code.
My point is that exceptions aren't as obsolete as it's somewhat painted in this video, but rather them being unwieldly is a result of little design effort put into them.
For instance, there's a design article from one of Java's lead architects (inside.java/2023/12/15/switch-case-effect/) that proposes an addition to the switch's pattern matching capabilities to catch errors in a functional vein - similar to how effect handlers work. And this is no coincidence: FP's effects and Java's checked exceptions are equivalent constructs in these paradigms (FP vs imperative). The sudden ease in failure handling incentivizes Java users to add checked exceptions to their function signatures more widely, achieving the same success in terms of safety without errors as values.
I do not agree with the premise of this video.
You know what else is just another word for goto? Return.
Each exception doesnt create a new control flow, there is a flow for correct values (return) and a flow for errors (raise). All correct values are returned to the function call and all errors are raised to the try/except block. There are no other flows besides those two.
Also, the "you no longer have to litter your code with try/except blocks" claim is technically true, just that you litter it with if statements and match cases instead.
Finally, with exceptions I can simply write the only correct path for my code, and if any of the calls I make fails, If its well coded it will rise an exception and I can catch that from the caller.
With errors as return values I must check each return value for errors at any step, whereas try/except allows me to handle all possible errors through the caller with a single try/catch statement.
Try/catch statements are great because they can cover an entire function call, and I don't care where the function breaks, I can simply code the correct path and handle any errors later. Its a much more effective approach and its only confusing when people use error values in return in languages with try/except statements or when people dont declare which exceptions their code throws for each case, and return values as errors solve neither of those problems.
Writing try/except over your entire function is a terrible way to write code. You won't know which part of your function is breaking, meaning debugging will be ten times harder.
Also, you don't know which functions are gonna throw, so you have to wrap EVERY function call with try/except unless you have full access to the source and the source of every function it calls. That's what I meant by the littering comment.
At least with errors as values you only need to add extra handling when you KNOW there's going to be an error.
@@IsaacHarrisHolt In any language with stack traces or exception values you definitely know exactly where an error occurs. I've never seen anyone wrap every function in an exception handler, but I have seen nearly every function have a conditional that checks for error values. Any half-decent API doc generator should tell you if the a function throws exceptions, you don't need access to the source code.
Most API doc generators can't tell you if it throws because most languages don't have anything to mark that. The doc generator would have to crawl your source code and the source of every function you're calling.
Also, yes, stack traces give you the location, but only AFTER the error has occurred. So you know what WENT wrong. Errors as values tell you every single thing that COULD go wrong.
@@IsaacHarrisHolt You won't write an try-catch over your function, if you intend to just keep it as error-condition. You just throw it up.
Also you know which function throw an exception, because exceptions are part of the functions signature in most languages with exception-handling and the compiler will enforce that you handle the exception - but only if one is thrown in the code you call.
There are relatively few major languages where exceptions are part of the function signature. I know Java has the option of doing it, as does Swift, but iirc it's not always enforced. And in languages where you don't have it, your tooling would need to recursively check every function call down to the native layer to see all the kinds of exceptions that can be thrown.
the thing is that exceptions has always been the wrong mechanism. If you use condition-restart system of common lisp you get all the benefits of errors as values as well as the benefits of try-catch and some unique to conditon-restart system like unwind the stack, identify the mistake, fix the bug in the execution environment and then rerun the stack and finish the original process. You can also store the entire stack to do it later or send it to another environment.
I'm not familiar with any LISP flavours at all, but that sounds interesting! I'll have to give it a look
It's a mistake to mix error handling with returning, no matter if returning Error values or returning multiple stack levels by an exception, because returning destroys the context in which an error might have been correctable. Have a look how the Common Lisp condition system works for a better approach.
I'm not familiar with it, but I think that a good error will contain the context required to debug it easily. I don't completely agree with your take here
I heard Crowdstrike has been experimenting with offensive programing lately ...
😳
Always argued with colleagues about their philosophy using try/catch and throwing random exceptions everywhere, instead of just returning an error-type, using a reference to "catch" the error in, or always returning a boolean to signal success of a method. All errors are expected and throwing UnexpectedExceptions is just a sign of a bad programmer. Fight me.
I don't want to fight you! But I'm not sure it's necessarily the sign of a bad programmer. A lot of it is down to the available tooling. Some languages don't have support for multiple return values, others don't have good pattern matching, etc.
Errors as values work best when you have compilers and linters that can point out the fact you've missed an error, or where your FORCED to acknowledge the error value
Exactly. Throwing generic exceptions with no info is the same as a webservice returning 500s with no body. Useless and bad code.
Imagine your returned error is always -1 and you're at the same spot.
I have to say, I think this whole discussion is borked. The video claims exceptions bad with stuff that is solved in some exception languages. Sure, if you compare with Javascript it will be bad. But other languages do exist, and they do stuff better. Similarly I could claim that errors as value are bad, because it is shit in C (the whole reason exceptions were invented is how bad it is).
But I do not. Because I use both exceptions and errors as value. And I do that even in one code base. For instance in Java or Kotlin I can (and I did) build types that can contain an error. But I can also use exceptions beside them. The only problem here is, that the function decides in which way it relays errors. But in a error-as-values language I have difficulties to emulate exception when they are the better solution (and they can be). At least since Go the defer keyword exists, which solves at least one problem, as it brings the finally-block of exception-languages into error-as-value languages. Does Gleam support defer?
Anyways, I wish for language support in which the caller could decide what way to use. It wouldn't even that difficult to begin with. The function declaration has to contain an indication if it can error. Then the caller can either call it in a way, say like this:
val optional_result = ?example();
This syntax could automatically wrap the result into an union or option (or whatever the language has): either an error or the correct result. You could unwrap after checking for the error-state.
Alternatively we could use an exception syntax:
val result = !example();
In this case result already contains the unwrapped result but in the case of an error it goes to the next catch or if not present the function with this call has to declare to be able to error itself to bubble it up.
That doesn't seem too complicated, why does nobody do it? Are people too much wrapped up in their thinking that one way of error handling excludes the other?
Firstly, sure, Java and Kotlin have something, but while you're forced to show that your function throws, are you forced to handle it? If you are, that's basically errors as values 😅
Gleam supports defer via `use`, if you write a defer function.
The reason language creators don't implement both is because it would add complexity to programs, and errors as values are generally preferred these days. Sure, C got it wrong, but that doesn't mean the whole concept is bad.
That said, the whole concept of exceptions? Pretty bad 😁
@@IsaacHarrisHolt Yes, if a function throws, you have to either handle it or bubble it up (by declaring the current function also throws). Well, what you say about C is my argument, only because JS has a terrible implementation that doesn't mean Exceptions are bad. And if you say that Java and Kotlin are basically like error-as-value you see that these concepts aren't as antithetic as you paint it. But you get back to claiming Exceptions are bad, based on nothing.
And as you say, modern error-as-value has become a lot better since it adopts more and more ideas of exception handling and both get closer. So implementing both interchangeable as I described shouldn't be that difficult.
Java and Kotlin makes two languages, and it's still not a great approach. Java has unchecked exceptions like JS, which arguably makes things worse, as checked exceptions lull you into a false sense of security thinking you've handled everything when you might not have done.
How is it less complex than a try catch? You still have to handle the error cases. It has the same amount of logic behind it.
In Java the functions that throw errors also have to be annotated, so you also see what error the functions throw in the same way as error return types.
True, but not every language is like Java. And complexity has nothing to do with how many lines of code you need to write, it's about being able to reason through your program.
If I'm reading a Java function, and I see something that throws but isn't immediately handled, I have to look at all that function's callsites, and all those functions' callsites, etc. all the way up the call stack to figure out where it's going to get dealt with, or even if it's going to get dealt with at all.
Because it's genuinely a nice looking abstraction over explicit nested if else's. I'm not for bullshit language abstractions but this one is just naturally useful as it's not merely syntactic sugar. Under the hood its monads but the syntax makes it useful without you even knowing the theory behind monads.
In Haskell it looks like this:
data Maybe a = Nothing | Just a
getDetails :: Name -> Maybe(Details)
getDetails name = do
personId
@@IsaacHarrisHolt How is thay any different from returning a result??????????
With a result you HAVE to handle it somehow. You also get better type safety, and useful functions you can use to manipulate them since they're just treated as values.
Moreover, results are predictable. That's the most important thing.
@@IsaacHarrisHolt If we are comparing it with checked exceptions in java (which is what OC was talking about), then there's little difference.
1. "You HAVE to handle it" -> you can just propagate it up, you just have to be explicit about it (which yes, it means you are forced to handle it). But that's the exact same thing with (checked) exceptions, you either do something with it or you propagate it up by annotating the functions with the throwing type, otherwise you will have an error (i.e. you are forced to handle it)
2. "Better typing" -> disagree. With checked exceptions you can annotate the function with the exact type of exception, and you can have multiple ones at that (and you are forced to handle then all). With results you do the same just with different syntax, but I dont see how one is better than the other.
3. "Useful functions" -> this is completely language dependent (just like exceptions itself, where most languages chose not to have them represented in the type system, while Java, for all of its flaws does - with checked ones at least)
4. "moreover results are predictable" - in what ways? For functional style programming I can see results being easier to reason about, but for imperative languages what are the advantages? For the caller it's the same thing, for the callee it's not any more unpredictable than returns in the middle of the function.
Results make more sense, imo, in functional languages, but for imperative languages I dont see any advantages of results over exceptions. The major difference is that with the latter you will usually have a callstack, but not with the former, so which one you use depends on the use case.
Errors with more info is good, and easily achieved with TryGet functions.
Question: If a team of software engineers were to fully adopt Gleam,
do they risk creating a product that could seemingly run like normal, but just not be coded properly to tell it is broken?
No more so than in any other language. With try/except, for example, you have the option of just not going anything with the error. It's the same with Gleam - you could just... not do anything when a process crashes or you get an error result. As usual, it's on the programmer to properly instrument their code and adhere to good observability practices
It's shocking to see so many commenters who don't get it, even after a good explanation. I'll add my 5 cents.
With exceptions you have your SUCCESS logic paths typed, but ERROR paths remain untyped. Compilers do not enforce exception handling and, in JS especially, there's no typesafety with try/catch whatsoever. So if you're into static types - congratulations, but without errors as values you get at most 1/2 of the whole type safety. And if you're not into static types... my condolescences to your career.
Finally someone who gets it 😅
Yes but even as someone who likes and regularly errors as types just blinding wrapping everything in a Maybe/Optionals or Result/Either is not the way to go. Especially when your project is sizeably large even in a language with the syntactic and compiler tools to handle complex errors like Haskell you may corner yourself into some unscalable and unrecoverable mess.
Use a mix of crashing and type level error handling.
@@samuraijosh1595 Haskell is a mess on its own, Gleam does this correctly with a single Result type. It is the way to go.
Yes, and just to be clear, you CAN crash. What's different is that, instead of resetting to a certain error boundary, you crash your process and let your supervisor start a new one
4:33 what's the difference here with try/catch (other than syntactical)?
Case statements will force you to handle every variant. With try/catch and exception-style systems, you're never forced to even acknowledge the error exists. In Gleam and other error-as-value languages, you have to face the error down when it arises.
Type safety.
@@IvanKleshnin Exceptions are as typesafe as the rest of the language. In Java or C++, exceptions are strongly and statically typed.
@@kered13 yes, in languages with typed and checked exceptions. Few of them exist, as you mentioned. But even such exceptions do not have any added value over Result type (throw is basically a goto if not immediately handled).
@@IvanKleshnin no. Gotos have a single target location, exceptions crawl back up the stack and they can be handled at ANY point along that crawl.
JS is not strongly typed, yet exceptions do have types, just not strong (just like the rest of the language).
also, you're supposed to throw useful information, and they definitely force you to acknowledge the error exists (unless you like topcrashes).
If you write code that acknowledges the error (a catch) and then do nothing with that information.... well, you could do the same in Gleam and any others like it.
Like there were not enough null ptr problems, so founders have developed the concept of error so generation to come have proper suffering routine even when coding becomes easier.
I'm not sure what you're saying here
@@IsaacHarrisHolthe’s saying you’ve created a made up problem that doesn’t actually exist😂
How'd you get that footage of me on my Lambo?
I'm in your hedge 😉
Can you print a stack trace in gleam?
In a debugging situation I would often like to log an entire stack trace for each error, so that I can fix my code.
Panic will print a trace, yes
Is Gleam good for scripting and test automation? I use Python and love Python but I do miss proper static typing. I could use TypeScript but unsure
It's not intended to be a scripting language, but you can run parts of modules if they have a main function. However, like Rust and other similar languages, you do need the project scaffold each time
the more languages we write, the more we find out that C was right all along 😂
Yeah but at least we're not stuck trying to decide the values of a million different ints 😂
@@IsaacHarrisHolt Absolutely!
Gleam probably has some of my favourite error handling all told
@@IsaacHarrisHolt Gleam is absolutely wonderful. Great Language.
100%! I love it
All of this goes away if you learn to use exceptions properly :)
Callback hell was solved in most languages years ago with the introduction of async (what a godsend, no more hadoukens!).
I've been using exceptions for years in the (*gasp*) weakly typed world of JS without issues. No random crashing, I know exactly what happened and where and I can recover from it if it makes sense to do so.
I always laugh out loud when I see a new exception-free language and then they explain panics.
It's not a case of "learning to use exceptions properly".
Fundamentally, these are two different solutions to the same problem. The difference is that one gives you upfront visibility over what COULD go wrong, so you can deal with it while you're writing the code, and the other tells you what DID go wrong when your code has already crashed.
Panics are meant for unrecoverable situations only. Exceptions can be and are used in both recoverable Andy recoverable situations, making it difficult to discern and differentiate.
What, can other programming languages not quit to desktop?? Also, what happens if someone used this code on top of one that just decided to catch whatever the new language tried and roll through?
I'm not sure I follow here.
Without going all the way to errors as values, I always found Java's checked exceptions to be really useful, something sorely missing in other languages. I see arguments against them, but I never really agreed and don't see how errors as values don't have exactly the same downsides.
Furthermore, strict null checking in Typescript has been really useful to me. Null is another value that can slip through unnoticed too easily in many languages. Option types work too in other languages, as long as you are forced to handle them.
Pretty much always use TS instead of JS now for this exact reason.
I also like how TS sort of lets you get away with enum types using a type field.
That is, you can do something like
type Result = {type: "ok", value: T} | {type: "err", value: E}
Now in any code using such a Result, access to value is guarded, but in any if statements that check something like if (r.type == "ok"), it will let you use the corresponding type.
Both good solutions! I'm not familiar with Java, but we use this style of error in TS at work, and it's pretty great. The problem is one of the functions you're calling in a library might throw, and you'll never know until it does.
On the null and option side, Gleam has both of those things! Nil is a unique type that can't be assigned to anything else (like () in Rust) and you have the Option type for values that may or may not be present
The problem with checked exceptions like in Java is that it doesn't work well with callbacks. This became a significant problem when Java introduced the Stream API. You can't pass any function that can throw a checked exception into stream map, filter, etc. without using some hacky workarounds. This problem can be solved with a sufficiently strong polymorphic type system, like C++ for example, but very few languages have that.
Im using vanilla HTML/JS for a project right now and I just want my dear sweet errors as values back. Gleam looks great, i might try it for my next project
Do it! Check out Lustre :)
Error handles must be able to support also errors like out of memory or stack overflow.
Those errors d not work as 'optionals/eithers'.
Also, what if you get an error while 'writing'' a file instead of 'reading' one? now you get a result Either but.... you are not going to look into that result anyway, since you wanted to 'write' you are unlikely to care about the result, and now the error is effectivelly suppressed. Not very offensive style.
In the case of a memory or stack overflow, your process will probably just crash and be restarted by a supervisor.
And for writing files, that's fine. You're still allowed to choose to ignore errors but at least you know they're there and may crop up.
Thanks for answering, I hope we can have a constructive chat.
@@IsaacHarrisHolt 'you are still allowed to choose to ignore errors' Sure, this is what we did with old C error code as returns of functions.. and everyone ignored them. Note that I'm not pushing for standard exceptions, I'm just saying that optionals/eithers are a step back, and that we need some way to make sure that errors are explicitly handled in one way or another at all times, not just when we care about the operation result.
- Depending on how 'crash and be restarted' works, .... it looks a lot like a try-catch.
- Offensive programming is really the right style in my opinion, but it makes perfect sense to do it in layers.
Where logically you have a few layers of master/slave patterns, where the slave computation can die, and the master computation can decide what do to, somehow protected by any corrupted state that the slave computation left behind. If 'crash and be restarted' works with actual processes, well.. that is too limited and relies too much on the OS, in my opinion.
In this case I'm referring to BEAM processes, which aren't tied to the OS. Erlang has the concept of supervisors which are essentially processes that look after other processes (I have a video on this if you're interested).
As for ignoring returned errors, sure, you could do that with C etc., but in C, when you DID want to look at the error, it was a lot more difficult to figure out what actually went wrong. You'd be relying on the author of the function documenting what int means what in some way, and different authors would have different standards for that.
Gleam makes this trivial by allowing tagged unions types as the error type, so you easily get the type of error and any context attached to it.
Errors as values are the way it was always meant to be
Agreed!
So, does anyone who uses C++ & wants their process to survive new failing actually do this in practice?
I'm not sure I understand the question
@@IsaacHarrisHolt Sorry, it's not quite relevant to your video, other than the idea of eschewing exceptions in favour of error values.
I spend most of my time writing C++, and in that context, I see people recommending such a coding style, but I've never been given a satisfactory answer as to how to do that in that language without it becoming extremely verbose.
The reason is that for my purposes, I want to write code that goes into a shared library that gets used by external programs. In that context, I never want to crash or corrupt the process (except for doing the former if I detect the latter), so that means I _do_ want to handle out-of-memory conditions (i.e. `new SomethingImpl()` throwing std::bad_alloc or `new (std::nothrow) SomethingImpl()` returning nullptr) by gracefully unwinding the stack, and then returning an error code via the C API that the shared library is called via by the host program, not simply assume that it can never happen, or just calling exit(1) or abort() or something, which is what I usually hear suggested.
The problem is that almost EVERYTHING other than 'mathy' functions probably needs to allocate internally to do its job, at least sometimes, and that allocation _can_ fail, so it makes almost EVERY return value an std::optional or something equivalent.
By writing my code using exceptions for error handling, and 'simply' assuming that ANYTHING (other than a few well-known things which are _documented_ to not throw) can throw, and using proper RAII everywhere possible and a 'transactional' style, I find that doing this is relatively straightforward without polluting too much of my code.
Amazing video! A super clear explanation🙌🏼🙌🏼🙌🏼
Thank you so much!
Ive been using return values as errors since veny long.
Its not new, i think this was the norm long time ago.
I do it In a sort of 'defensive programming' way.
It doesnt depend on any special language feature. And i do things a bit differently than you.
Im quite proficient with exceptions, but i like my way better.
And i think it works really well.
I make games with unreal, and (besides exceptions not being available) a critical error can easily crash the game. With my approach the crashes are only due to very low level stuff on the engine. And i usually patch those cases.
Yes, errors as values have been around for a long time. That said, certain type systems make it easier. For example, C errors are typically just ints, meaning you have to then use some sort of lookup table to figure out exactly what went wrong in any meaningful way.
So instead of a try catch block catching explicit errors, use "use" for that block's length plus a switch or whatever later on. Seems the same with extra steps.
The difference is that try/catch is pretty much always optional. At least with `use` you're forced to write it in, acknowledging there's an error possibility.
@@IsaacHarrisHolt Then every function call should have 1+ line of error handling. How horrid.
That's not horrid. That's... the point. It forces you to think about all the ways your program can fail
@@IsaacHarrisHolt While custom ECC and triple redundancy to withstand cosmic rays might land me a job at ESA, i just want to parse some JSON. A crash is fine. Does Gleam even have a formal prover like Spark Ada though?
I feel like you're being deliberately obtuse now 😅 as mentioned in the video, you can still choose to crash if you want
I never understood the reason to catch errors.
If there is an error in the system, the code shouldn't catch it and handle it in some weird workaround ways. The code should be fixed so that the error cannot occur anymore.
When is a try catch actually truly useful and not due to bad code?
There are always things that can go wrong that are outside of your control. You might be trying to open a file that doesn't exist, or your internet connection might drop out in the middle of a HTTP request, or a million other things. You can't prevent every error
Exceptions are for code that cannot be handled locally. If you encounter out of memory error, you don't want to handle it in caller functions. You want to unwind stack until handling lack of memory makes sense.
exceptions are for...... exceptional situations!
Seems like older C/C++ API's got it right the first time. We use a lot of async boost in our server application and we just default to the error_code version of the methods unless we specifically need to handle exceptions. Its much easier to reason about an error code, especially in callback based code.
100%, but it's also nice to have named errors in the form of enums!
Great video! (it came out 5 minutes ago and theres no way I could've watched it all)
Haha thank you! But 2x speed would work 😉
No. A function should only be expected to intentionally do what it says it does. An Exception is an exception.
Yeah, and with errors as values, we're just saying that something could go wrong :)
pardon me, the pink smiley star... is it for a language?
It's Gleam's logo!
@@IsaacHarrisHolt thank you.
Well, it appears more likely Rust to me...
Gleam has some similarities, for sure!
Gleam itself is written in Rust
True! There's definitely some inspiration
Hello undefined! You have NaN new messages.
Ah, sweet, sweet JavaScript
Issac is to Gleam what Akademiks is to Drake
I'm hoping this is a good thing 😅
Isaac is to Gleam what NoBoilerplate is to Rust
3:35 Bubbling the error is very common in functional languages. Most (if not every?) of the error types are monads, which means that you can define a way to compose functions on those types, and if an error value occurs, carry it to when it's matched on.
Edit: Ok it's described on 5:04, nice ;)
Yeah, you'll frequently pass it up, but it doesn't just magically appear three functions up the call stack like it would with exceptions :)
@@IsaacHarrisHolt It works differently under the hood, but the result is very similar - a nested function can return an error but you only have to match at the very top, just like you would do with `try catch`.
Of course, but with errors as values you have to explicitly pass it back up the call stack. With exceptions it just kinda... appears at the closest try/catch that matches it
@@IsaacHarrisHolt No, you don't have to do it explcitly, that's what the 'use' keyword does for you.
You're still passing it explicitly. `use` is just a language feature like Rust's `?`. You're still making the choice to say "I have this error but don't want to deal with it, so the caller can have it now". It's still explicit
Using errors as values instead of exceptions feels like trying to reinvent the wheel, but using triangles instead. 🤷♂️
Exception handling is crucial for creating robust and maintainable applications and libraries, particularly in environments that have evolved over many years or decades. In such environments, development errors are more likely, especially when incorporating dynamically linked third-party code at runtime. Exceptions provide a structured and standardized way to handle unforeseen situations that can arise in these complex systems.
While using errors as values can be appropriate in specific scenarios, such as in performance-critical code that has been thoroughly tested and is unlikely to encounter unexpected issues, it is not a one-size-fits-all solution. Exceptions generally offer a more reliable and maintainable approach for the majority of applications, enabling better error propagation and handling.
I agree with some points here, but I disagree with exceptions handling being more readable. If I have to go function hunting to find where a particular exception is coming from, I'm having a bad time.
Also, in most languages, you lose type safety when dealing with exceptions, which negatively impacts your ability to correctly identify and handle errors as they arise, leaving you playing catch up while your software's in production.
@@IsaacHarrisHolt That is mostly true, but what I wanted to say was that a hybrid approach will get the desired result.
Combine exceptions and error return types for robust error handling. Use exceptions for critical infrastructure and initialization errors that require halting the system, ensuring immediate resolution and preventing instability. Employ error return types for business logic and non-critical errors, explicitly managing and propagating them to enhance type safety and predictability.
This hybrid approach balances the concise control flow of exceptions with the explicit error management of return types, optimizing performance and reducing complexity. Clearly define boundaries for each method, document guidelines, ensure consistent handling, and implement thorough testing. This strategy supports maintainability, type safety, and efficient error management in complex systems.
That's basically what Gleam does with a panic. It's not an exception, because you can't catch it, so it's only used to stop the program. I wouldn't say that's a hybrid model.
@@IsaacHarrisHolt I don't see how the erros as values are solving the "function hunting" problem. You say in the video that most of the time, you'll just be propagating errors up to the caller, which I agree with. But then, a few layers up, when you actually handle the error - how do you know where it originated? You have to go function hunting as well, no?
Maybe it'll be a specific type that you can associate with a particular function. But then, if you call multiple different functions, how do you propagate all of their different error types upwards? There would have to be some common error type which can hold all of the specific functions error types. And then we're back at the question: how do you find the function this error came from?
This weakens the type safety argument as well. With errors as values, if a function wants to propagate errors up, it'll need to use a common type for all errors that could occur within it. In many exception-based languages there is a common base class/interface that's enforced for all exceptions. Is there really such a big difference in type safety?
There's a huge difference in type safety. It's unlikely that you'll just have a single error type that everything uses - if you look at the stdlib, you'll see that the functions there generally have their own special error types, and that's generally encouraged.
I see your point about the function hunting argument, but you also know that the functions above will HAVE to handle the error in some way, even if that is just returning it up the call stack. With exceptions you don't get that guarantee.
When you're writing a function that throws an exception, you're making the assumption that everyone who then calls that function will handle the exception somewhere and it won't crash the program.
With errors as values, you're FORCING callers to handle the error somewhere.
Wow that use statement is kinda sick
It's great! You can do all sorts of things with it
We are back in the same spot in history where we have error codes like in C++ era. But honestly, Error enums sounds like the best of both worlds. I always hated try-catches.
Agreed! Handling random integer errors was a bit of a nightmare
I wanna learn gleam cos the star looks cute
Lucy is adorable and we love her
@@IsaacHarrisHolt 🤩
5:05 "its just a function, no special syntax required"
5:19 "luckily for us, gleam has a special syntax"
edit: @ 5:50 this is literally just (imo) uglier ? syntax. ill stick with a singular way of declaring variables
Use isn't for declaring variables! It's for flattening out callbacks :)
Why is this video tagged as python lol?
Exceptions are special values on certain languages.
A lot of the tactics you use are pretty similar to how exceptions.
I think the satement that youre saved from unhandled errors is not accurate.
Fixed! Thank you.
Imo, exceptions are my main pain with C++. The langage spec is convoluted and over complex due to the fact that it's a systems language from the 80s that needs backwards compatibility with C and its past self? Sure. Does it have 5 ways of doing almost exactly the same thing? Sure. My main problem is that error handling is awful. You have no real way of knowing if a function will throw or not (most functions can't be nothrow due to memeory allocation, and most people don't mark it anyways). That opens so many possibilities of forgetting to check an exception and it randomly crashing your program. It's really annoying, especially since it's pretty nuch the only way to error handle from a constructor (yes factory methods are a thing, they're not widespread and have issues)
Exactly! This is the problem that errors as values solve. I know C technically uses ints for errors, but having a named enum is far better
I think try-catch was born to pay more attention to the code that can crash everything, in pure C many call functions without managing the return value, the compiler will use a temporary variable and will not give any error at compilation time, the program then crashes and nobody knows why. C++ try-catch should help to pay more attention to the code and specify the error better, in addition to a maintenance issue: you can use ctrl+F to search for "try" instead of having to create a common standard for error variables.
I don't think try/catch works, though. Sure you can search for where it's happening, but that's not helpful if you don't know where errors are coming from.
So, do you mean the widely known and traditional C-way?
int err = getaddrinfo(SERVER_HOSTNAME, SERVER_PORT, &hints, &result);
Not sure why the need to present it in a light of a new language
The C way works, but it's nicer to have named errors in the form of enums rather than having to try and decode an int
C is missing the monadic syntax sugar shown at 5:53
Have you ever heard of logging? 😂
Sure, what about it? Logging complements good error handling - it doesn't replace it.
The gifs are unbearable. Hard to watch. Please just show code.
There isn't necessarily code for everything I say, but I'm trying to reduce the number of GIFs. That does mean the videos take longer to make, though 😅
@@IsaacHarrisHolt Sorry. I am being a drag. I enjoyed the video otherwise! You're good! It's just that these things get old so fast. Life as a meme is short and brutish. Then again, maybe someone else would love these things. You can't please everyone all the time. I don't know. Just do what you love and serial moaners like me can go fork themselves with rusty kitchenware!
Some get really distracted with stuff like those gifs. Im one of those.
I end up not even watching the video.
I rather see nothing if theres nothing that adds information that i need to watch.
Superb production and delivery. Writing was decent 😉
Writing was the best part!
just make every function return a monoid
I'm not smart enough for that 🤯
You literally use monoids every day, we all do, (numbers,+,0), (numbers, \*, 1), (lists, append, []), (bool, &&, true), (IO, >>=, return)
_(ok that last one may be a little controversial)_
I haven't written enough white papers to know what a monoid is 👀
@@IsaacHarrisHoltat 5:53 you have written a Maybe Monad computation. So you're already good 😂
Awesome!
The future? Sum types for success/error cases has been the standard in the ML family for decades.
Yep! And the rest of the industry is finally catching up 😅
I heard an experienced developer say that Gleam's match is better than Rust's!
Gleam's match is fantastic!
everyone gangsta until Optional option = null
😎
I just want something like Python but with Rust's type system and exhaustiveness checking, so error handling without exceptions becomes idiomatic.
Sounds like you want Gleam 😉
Tom is indeed a genius :D
The geniusest
The Inner JSON Effect mentioned
Always
Tom's a genius.
We're all just quiche eaters under his gaze
More libraries for syntactic sugar... nah I'll pass. I'm good with wrapping functions in try-catch
No libraries here (except Snag), all pure Gleam!
No this is provided in the base for gleam and many functional languages like Haskell, Ocaml.
erlang is offensive, got it
Mostly to my eyes
C has always done it like that
Ah, but in C you have to try and decode what all the different integers mean
@@IsaacHarrisHolt True.
Still, Go's and other languages and paradigms way of handling errors by returning error values is a lot like how C does it, it even seems like Go was inspired by C in that way. So, moving away from exceptions and back to handling errors as values isn't really something new, it's just getting back to basics.
Yes absolutely. The industry is finally realising that we were nearly there all along. Gleam is nice because you have the enums, though, which helps a tonne
👍
Thanks!
I don't like how you present all of this in a way to only ever mention Gleam, and not any other programming language. Especially at 2:35 - the "other language" is literally the language Gleam's compiler is written in. Yet, you go out of your way to avoid ever mentioning it, despite most of the features presented being literally copy-pasted from it. This is dishonest.
Feedback taken, thank you! But I am a Gleam TH-camr 😉
@@IsaacHarrisHolt yes you are, but a) this video is advertised without any indication that it is Gleam specific, and b) while I understand not wanting to repeat "like in Rust" in almost every section, not saying it what-so-ever, or substituting it with "other language" is even worse.
But it's not just Rust. I said "modern programming languages", and showed Go, Zig, Rust and Elixir. I don't specifically call out Rust or Rust-specific features at any point. I believe I mention that you might be familiar with the `Result` type from other languages, but it's not like Rust invented that type or that idea - it's been common in functional languages for decades. Do I have to call out EVERY language that has ever used a `Result` type to encapsulate errors?
Also, fair on the packaging point, but you're probably seeing the 1 thumbnail variation of 3 that doesn't include the Gleam logo, so I've now got some extra feedback on that experiment. Thanks 🙂
@@IsaacHarrisHolt to be clear, I wasn't arguing about the "modern programming languages" part, although now that you mentioned it - I didn't really read that code on screen, and for sure didn't recognize it as four different languages. Putting their logos near the respective code snippets would have been great.
As for the "Result" type, I repeatedly hear "from other language" in singular, although without a particle. So... My bad for assuming the worst, I guess. And I really cannot hear the plural "-s" there, no matter the volume.
I guess the argument is resolved now, sorry for throwing accusations (I repeatedly can't remember not to do that, when I'm right about to do that).
No worries! It happens. And yeah sometimes the S gets cut off - I really need to look into my audio settings!
fucking hell how much budget does gleam have for all this marketing...
Like $0, we just like it
This was funded though lol but very little sponsorship currently to my knowledge
As Ben said, none! It gets covered because it's a great language.
@@bcpeinhardt I'm just extremely surprised that almost every tech channel suddenly started covering Gleam in their videos
It's because Gleam is new, exciting and fantastic! :)
Skip the memes please.
Working on it!
We have successfully returned to ideas from the C programming language and decorated them as something new once again! To be fair, while the idea isn't exactly new, gleam definitely executes the idea better then C does in regards to ease of development.
100%! Not having to handle a million different ints is helpful, and the fact we can have named errors makes life much easier.
ah yes, Other Language, my favorite
Yeah, probably coulda named those in hindsight
@IsaacHarrisHolt ah I found it pretty funny! It was clear what Lang you meant
Actually, there are a lot of langs that used `Result` before just Rust, so it wouldn't have been great to name just one. I would have had to name all of them, which might've taken a while 😅
@@IsaacHarrisHolt Sure, but most people did hear about it _from_ Rust, even if they didn't invent it
Stop doing stop doing videos..
I've done... 1 (one). This one!
I found a way how to deal with your memes - 2x 😌
Hahah fair enough!
Just use Rust
High effort engagement here
No.
Gleam and Rust solve very different problems. They're not interchangeable
@@IsaacHarrisHolt I fundamentally don’t agree, but get where you’re coming from.
They have some overlap (e.g API development), but you'll have a much better time building a distributed, multi-threaded system in Gleam, since that's what the BEAM is built for.
Similarly, you'd probably get along better with Rust if you're writing an operating system or something low level. The languages have different design goals
I honestly don't like this kind of videos. Don't tell me what to do, if throwing errors works, then it works. I won't stop doing what I know works (and works well at that)
It's an opinion video! You don't have to agree with the concepts, and you're allowed to have your own views
Would love to see more code and less memes. Constantly repeating gif memes are cringe and distracting tbh
Working on improving the stuff-to-gif ratio, but I'm always low on time so need something I can edit quickly!
gleam is stinky doodoo defensive programming. Erlang doesn’t need static typing. Also Gleam runs on BEAM but didn’t adopt OTP. Trash.
I think you'll find that Gleam did, in fact, adopt OTP. You can check out the gleam_otp package to find out how wrong you are :)
@@IsaacHarrisHolt Have you read the README for the same gleam_otp package you mention? «This library does not currently replicate all of the Erlang/OTP functionality».
Just use elixir. It's alive and well. "Type safety" cancer is not what is needed for distributed systems. Joe Armstrong would've sighed with disappointment if shown this idiotic language with no point whatsoever, with no contribution whatsoever to the BEAM ecosystem or the distributed programming experience. It's not even a lisp. Cringe.
Of course gleam_otp doesn't have feature parity with Erlang. It's a much younger language. Thinking it's going to be exactly the same is naive.
And on your point about typing, yes it introduces some challenges, but it also fixes a whole host of issues you would otherwise see in your programs, distributed or not.
I would implore you to actually try Gleam for a reasonably-sized project and see how you like it. Badmouthing something without trying it is close-minded.
@@IsaacHarrisHolt I have erlang and elixir experience, and I tried gleam. It's nothing new. Elixir fixes many aches with erlang, including type aches if there are some in your case. And it has macros. What is the availability status of modules that contain behaviours and macros from other beam langs? Does gleam still need the same fat layer of glue code to fix its inherent incompatibility with libraries that contain too much "let it crash"?
Gleam is absolutely okay with "let it crash". It still supports supervisors, process monitoring and most of the requisite stuff there.