In js, most libraries(native ones as well) throws errors that you may not know about. Sometimes this errors aren't even the native Error type. In Golang you know exacly what kind of error you are expecting, I like that explicitly.
@@mdbk2 it is much easier to ignore a return code then an exception. If you miss an exception it will be caught on top level so you know it did happen. if you ignore result code no1 will know it did happen.
I absolutely love Go error handling. Honestly I hated error handling when I started learning programming because I started with JS. When I started Go, I hated the error handling until one day after writing a service in it and realized how nice it is, looks bad but who cares? It works well. I agree though that Rust does error handling better but Go’s is super simple along with everything else about the language
Though rust error handling does a bod job about source of error unless you set the proper env variable in debug. With Go and wrapping, you get better pinpointing. From what I recall, Zig has the best error handling - the best from both worlds.
Disagree completely. Writing exception handling in Go is awful, because there is so much code that is NEVER executed, but is just there "in case" ... Just yuck.
@@Tony-dp1rl what do you mean by code that is never executed? If you know that it is never executed, just use underscore to ignore the error, otherwise it is a branch worth handling.
Both Go and Rust use monadic error handling: Rust uses the Result monad while Go utilizes a monad known as "Writer" (which basically means a value with some other accumulated value, which in this case is the error). Rust is better in this sense because you cannot represent invalid states. On the other hand: in Go you could return `(nil, nil)` or both an error and a value. The result of this, is that error handling in Go feels more imperative and in Rust more declarative.
writer is when you append things to a list, hashmap, etc. (err, value) is just a pair, ie an anonymous product type. writers are useful outside of FP languages when you want to accumulate multiple errors or accumulate operations and execute in bulk or mock io.
10 หลายเดือนก่อน +3
Agree, add to this that the Go compiler does not encourage (force) you as a programmer to handle the error case, it can be happily ignored. Whereas Rust compiler forces you to handle all cases making it impossible to access the return value (compile-time error) without checking for an error first. This is a huge win. The `Result` type and pattern matching in Rust are simply awesome.
Then ironically when you use the return value from the JS function that could be a value or null, you'd still have to do an "if value === null" and handle that case too lol
What a stupid post upvoted by people who don’t use JS/TS. Null is never a native output in JS. It doesn’t happen on its own. Some programmer simply decided to use the “null” object. All empty references in JS are undefined. Don’t blame the language because your coworker or your own stupid face decide to return “null” objects either due to a cargo cult believe that value is useful, or because they need to signify an assigned but unused value.
Having coded full time in js/ts for 5+ years and 3 months in go, I'm 100% convinced go's way is better. Never want to go back having build production apps in both. In go, I know every error has at least been acknowledged if not handled correctly.
Go doesn't force you to handle it. Just write a bunch of ugly IF err != Nill checks. Proper wrapped error types are cleaner and guaranteed they're handled.
That's why I said at least acknowledge it. It's up to all of us to handle errors smartly. But at least I always know if a go function can error.@@natescode
@@natescode Every languages have the way to write ugly error handling. It's just that TS never tell you if it gonna throw or not. Go at least shows explicitly that error is possible without using any LSP, and if someone write ugly check it's just gonna be very obvious.
@@OnFireByte It's not explicit when the majority of nontrivial functions in your program are propagating an unknown union of errors up the stack. The issue with error handling in Go is you don't know what errors are being returned. It's nothing like Result in Rust.
I like errors being part of the return type. My problem is that instead of using typed unions / associated value enums / algebraic data types, which is the correct, type safe way to deal with this, where accessing the value requires you to branch on having an error or not... they made it a tuple of nullable values. I guess it's good for a simple language that doesn't want extremely basic and important features for modeling data like being able to represent mutually exclusive states. (I'm salty.) That said, e.g. LLVM does errors like Go (with a custom wrapper return type) and it's really fun. On the other hand, I also really like Elixir style error handling. Elixir is built to do incredibly fault tolerant distributed systems where lots of things can error. So the idea is don't handle individual errors; any time there's an error, just recover from a known good state.
If you've ever worked professionally in Go _and_ in another language without error-by-value, I strongly think that most people would prefer EBV. It significantly reduces the cognitive overhead when you need to jump into an unfamiliar service/repo. No surprises on where errors might pop up from.
I like Rust's Option and Result much more than Go's err. Usually (in code I write) if I'm handling an error I'm dealing with data from the code that may have encountered an error. A Result makes me decide how I want to deal with errors before I can get at the data (I can return the error with "?", panic on errors with ".unwrap()", convert to an Option with ".ok()", map Ok()/Err() values, do clever iterator stuff with arrays containing error values, and otherwise gain a lot of flexibility by having the error and data bundled together).
Before Go I did a bit of Delphi and C. I hated try-catch in Delphi, I didn't know where to put, I didn't know where exactly it came from and if you did not set it right initially it was even worse to find what is happening in production. C was such a breath of fresh in regards of errors - very simple and understandable, but really annoying to deal all the time with memory allocations and leaks so when I found Go and had the chance I switched all the codebase to Go in the next few years. This was 5-6 years ago, I still think it was the right call. I have colleagues that when from Delphi to C# and to this day we have this debate where they consider try/catch superior
I am primarily a C# developer. Exception < (value, Err) < Result, simple as that. The problem with exceptions is as soon as they are possible in a language, they can happen anywhere and everywhere, for any reason, without warning. They are awful for having certainty about how the program will behave. Results are superior especially when the language has the ? or equivalent operator to pass the handling up the chain and otherwise enforces the handling of both cases, but I frequently try catch Exceptions in C# as early as possible to convert into a Result type just to get a little control back
If there is something I really love it's jumping into a new codebase and the previous developer decided to make a "resilient" and "error handling" application that just does err != nil checks everywhere but instead of actually handling it in a meaningful way just returns it up the stack. Now I suddenly get a error code somewhere but I have no idea where the error originally happened or what could have caused it because the stack is unwound and the program already exited... I really love to then start hunting down all places where the return err is used and add logging just to be able to try and catch it 😍
Absolutely right! The messed up part is, lot of Devs prefer smart looking or cleaner code over secure or more stable code! Btw I love go's error handling or generally, "Errors as value" over old school "Exceptions"
Old school would be to pass the error through arguments, or return an int with negative values, and is arguably worse; If you need the error message you then have to call `strerror(errno)`. Couldn't be anymore magic in my eyes. The problem is not exceptions but the language's ability to provide proper ways to deal with them and convey information about them. A language should provide ways to mark functions and things like pattern matching, operators, guards, etc. to handle them. Not just try/catch, which is arguably the worst way to deal with exceptions.
@@dealloc try catch is what makes exceptions what they are. Maybe there is a different syntactic way to express this, but exceptions (in the context of things you "throw") will always have the undesirable behavior of being inherently different than other normal values. Its the ability to throw something up the callstack implicitly that (in my opinion) causes issues, not just the scoping issues with try catch blocks. Being able to throw an exception is essentially a goto statement without an explicit label. They can make the control flow of an application hard to understand and they inherently break locality of behavior. They are also harder for static analysis tools to reason about. I will say that Java forcing explicit definitions of what exceptions may be thrown from a method at least gives more explicit information and helps with static analysis. But it still suffers from the non-locality of behavior issue. Having errors as values means you don't need separate constructs to handle errors. They can be handled like any other value in your system, which leaves a lot more flexibility in how you handle your errors. Even when exceptions are values (like being objects in Java), you still have to use special syntax to extract them and pass them around, and I've rarely seen that ever done. You essentially create two separate control flows for your system, one of which is implicit.
Although returning a tuple is wayyyy better than throwing and catching, I still find it really awkward to handle. I think Rust and functional programming languages got it right by using a Result wrapper type / monad. That way you are sure that is one or the other and it cannot be the error and the value at the same time, moreover, you get methods and functions to deal with error handling in a really logical and secure way.
Thanks a lot for the vid. I'm only starting with Go and I was quite confused that error handling in Go was criticized this much, as to me it looked fine after years with JS. Yes, annoying, but as someone said on the internet "at least it makes you pretend you thought about how to handle potential errors", which sounds like a win for me. Maybe I haven't yet written enough Go and I'll have a better idea of where this pattern falls short as I run into more cases of handling errors, but for now I'll move forward with confidence writing Go and not stress about that error handling is "ugly". Also, thanks to everyone who added perspective in the comments sections, I learned a lot while reading about nuances of this pattern!
Exactly. It is comparing apples to oranges. Equivalent Go code to "const val = await foo()" would be "val, _ := foo()". See, it is even shorter and easier to read :)
@@zaneearldufour You don't use panics for error handling typically, you use panics for situations where you can't keep on running your program and thus you panic and see the stack trace of where it panics. I.E when you don't set the port properly for a server (use letters for example) and you have to stop the server so that you correct that before you keep on going with running the program.
@@Gusto20000 I have not written Go (I do write Rust btw), so I don't know what's the value of `val` when err == nil. But isn't the only logical choice being that it must be nil? In that case, using val should result in some sort of panic shouldn't it?
As old-school C programmer, I can tell that this sort of error hanling was in the C forewer. You always return an error code and then you have an if statement to handle this error code. The Go haven't invented anything new here, except now you can separate an actual function response from the error that gives you this nice shugar. The main problem with this error handling is precisely a mess of if or switch statements after each function. You need to handle every error and then pass it on to the caller by wrapping it into another error what happens (face it) in 80-90% of the cases. But this extra code can be source of all sort of bugs because it is unnecessary operations that language forces you to do after every function call. Not only that, but you cannot chain call the functions this way. Lets say you have a function that return a string and another one that parses this string and returns integer. Because you must handle errors, you will need to process the first function response then call the second one and process its response as well. While you don't care if the first function didn't return a string or if the string wasn't a valid integer and the second function have failed, you will have to handle error twice and return a nil, err in both cases. This is why exeptions in C++ and Java was so great, it eliminates the need of doing that. You can handle errors in one place, you don't need an if statement after every function. There is no real problem to find out what function have failed, all you need is to know what type of error have happened and you can achive it by throwing different clases of exceptions. And thanks to inheritance you can group similar exceptions into groups by subclassing them from the some base class and handle this base class. It gives you way more flexibility on how to handle errors in your application and where to handle them. I will not be surprised if exceptions will be introduced in go at some point.
At this point people just need to start implementing their own Maybe/Either/Result Monad, its not that hard at all, its actually insanely simple, Just a couple 5-6 functions and you're good to go. Monads can be implemented in any language and they're insanely usefull (Just look at rust)
One thing I did in my C# code was add an extension method to Task that just is a wrapper around a try catch that returns a (T, Exception?) tuple that lets me do this and wrap existing methods so that I never get exceptions
If you want to handle errors by value in javascript you can adopt that convention and return a tuple like in go. Then the code will look very similar to go. The code in this video is complex because you are translating error via exceptions into error via return value. If you use exceptions all the way through then in javascript you will just do .catch on your calls to foo and bar and do your handling in there which is imo a lot cleaner than go
I think this moves onto another interesting point, exceptions are meant to be exceptional (or so the saying goes), while Go's error is par for the course. Many developers use exceptions as an 'all hope is lost' - because if it wasn't that type of situation then it wouldn't be exceptional (stacktraces are expensive afterall). I've seen many a function written that instead of throwing an exception, would return false, or an error object, as the problem that arose is expected and part of the potential flow. From this video, I get the impression that an error is an error, no matter where it comes from in Go's paradigm.
``` var err error if err = something(); err != nil { return errors.Join(fmt.Errof("specifics %v", value), err) } var data Blabla if data, err = other(); err != nil { return errors.Join(fmt.Errorf("some specifics %v", value), err) } ``` You did talked about this sir That's how you create wonderful errors management in `go`
Either your effects are in your types (tuples, monads, a failure ADT) or they are in their own effect system (exceptions, signals) I'm glad you mentioned a monadic approach. It comes down to the same tradeoff: I want control -> handle it all in line I want ease of expression -> abstract error handling from the "happy path" computation Different tools for different jobs
To be fair, as someone who regularly grumbles about Go's error handling I'm not advocating for it to be more like JavaScript's with Try catches, I just want like a '?' operator that would be the equivalent of "if err != nil {panic(err)}" and another like "?{return err, ...}" that would be the equivalent of "if err != nil {return err}" or "if err != nil{return err, nil, etc.}". When I need to do some custom error handling, Go is the best, but when I'm just testing something out or I don't expect any errors that shouldn't be returned I'd rather not have to constantly copy and paste the same codeblock.
? should not panic, it should propagate. The problem with pure propagation is that you have leaky abstraction. If you treat each package as standalone OS library, it should propagate typed errors from its package, hence just using ? is not applicable in that package's implementation. Rust fails at that as well. I would like tike have `.ErrOnFail(err, "%w: error creating database user: %w", ErrInvalidState, err)` which would propagate a wrapped error.
I think it really depends what type of software you are building. For example: If you are building a basic CRUD Web API , I would guess +95% of the errors are "unrecoverable". Like if your database connection fails, what are you going to do? If you have bug in your business logic? If you failed to validate the request parameters correctly? For all of these cases there is no "recovering" from them. So having such fine grained error handling makes no sense. You can handle all of these with middleware (single try/catch). It's bit different when you are building something where not-recovering isn't really an option, like an operating system, IOT related or something where there are no transactions to save you. It boils down to using the right tool for the right job.
Totally agree. Errors in backend are unrecoverable for the most cases. The only things can be done are: report or log errors, rollback DB transactions and show users sorry messages. It's better to return meaningful messages for user-input-validation errors though.
I think this is pretty much true across the board, regardless of domain. I mean realistically, what strategies are even available to recover from most errors? Trying again?
@@isodoubIet Yeah, I guess "recovery" is not the correct word here. It's more like generic and non-generic error handling. Most errors can be handled in a generic way but I can imagine situations where application/tool has a lot of internal state and you have to essentially rollback the mutations manually when errors happen. I can see try/catch being really clumsy in those situations.
@@z0n_ I suspect this is not possible to in TS/JS so this might explain why so many in this audience have issues with exceptions, but in C++ you can design things so that everything rolls back automatically in the case of an exception. When an exception is thrown, stacks will unwind and destructors will run. You then have several options for how to deal with rollbacks. You can divide your work in a prepare/commit fashion (so that all possible exceptions are thrown in the "prepare" stage, and all changes in system state happen in the commit stage), or you can have custom types that explicitly roll back changes when destroyed, unless explicitly dismissed. A third option, which may sometimes be possible, is to not rollback completely but to allow things to be in a valid but unspecified state after an exception is thrown. Either way you're never rolling things back manually, which is pretty clunky no matter what error handling strategy is being used but surely unworkable in the presence of exceptions.
What if 95% of errors in your app just need to be logged and corresponding http status code returned? It's more more convenient to let those errors bubble up to a centralized error handler instead of repeating err != nil checks all across the call stack. The other 5% are usually retries/circuit brakers etc. which are handled by libs (talking about web services in this case)
this is the one for me, in most of the code i write, 95% of the time an error happens i just want to rollback the transaction, do some logging, and respond with 500, all of which go in a middleware. that way, i dont have to think about exceptions most of the time
When I was introduced to Java and had like 6 months to start writing production grade code, almost 20 years ago, I was annoyed one had to try-catch all the time, but ohhhh boy!! Did that prepared me for future experiences with post-Modern hype driven development languages... It got drilled in to me that anything can fail at any time.
This totally make sense but is a bit weird though. I mean, the go-style error handling here is just what all language without try-catch simply do. C also do that, just without the tuple-like return syntax.
The mistake of the Go design is that Error is a separate thing that needs its own interface. Instead of treating "error" conditions as Errors, you can just treat them as normal conditions.
The problem is actually because of the error, you can't use the result immediately in the same line, ever. Even simple things like string to int, getting a value from a map, etc.
@@samfelton5009 C has more or less when you use the return value for errors. if(err = f(n)) {...} When err is a non-zero int, the if evaluates to true and you deal with the error. The "downside" is that you have to use pointers as out-parameters for setting normal results
@@samfelton5009 zig. In zig instead of "if err != nil return err" you have "try". The try in zig is not exception handling, it just does the go err != nil part automatically. For example: try func(); instead of: value, err := func() if err != nil { return err } that zig version is the exact same thing as the go version
Neither matter as much as expressing oneself clearly, though. When you drop a platitude using a made-up expression like "code pragmatics" which people can only guess at the meaning of, you already failed.
He touched on it right at the end. The really problem is not that it returns the error. Returning errors rather than throwing them makes them explicit and 100% better. The real issue with go is that "if" statements everywhere is such a shit way to then handle those exceptions. Error handling should be built into the language as a first class citizen and languages like Scala that allow you to then pattern match and using things like map, flatMap, for comprehensions, etc are such a better way to handle things. Sometimes you don't need to care about every place it could fail. Sometimes you want to just write code that says do these things as long as they keep working and let me handle the failures at the points I care about. While is why using a Monad and gaining fluid support for chaining map operations is great.
There's a package called neverthrow that's quite cool, even though it's a bit awkward, it adds that extra layer of security to JS/TS. Takes the mysteries out of errors, and forces you to handle them. To me personally though, although I like the philosophy behind Go's error handling, my issue is that there's no shorthand for the err = ... / err != nil stuff. That pattern is just so common that I just feel it can be shorthanded to like an "or". Something along the lines of `someVal := someFunc() or { handle error }` and the compiler just forces you to handle the error.
I hated Go's err handling. Until I launched my first maybe small but serious app and there were no errors, fatals, boundary cases or anything like that. I just couldn't believe it! After 15+ years in IT, one's used to getting unexpected errors, NPEs and similar snafus all the time, especially during first tries. But in Go... NOTHING. At first I thought I screwed up so badly, I missed some errors. But there is the point - it is impossible to miss errors in Go, I correctly managed all situations! It was love from the second sight. ❤
Depending on the context, implicit error management can help you write simpler code, while explicit error management can help you write more robust applications. I would argue that it is almost the same with garbage collector and RAII. With garbage collector and/or RAII, you can write simpler code. But you are pushed/forced to ignore errors in destructors so your application is not as rigorous as it would be with explicit resource management.
This days I tend to use a library like purify-ts to wrap my Promise codes in Javascript. I find it insane not to be able to reliably know what errors a function throws or even be forced to handle it. This however means that I'll have to be using creating more functions to handle errors (like .then promises handles)
hmm, Go error handling used to make me suicidal cuz it's a pattern I'm not used to. After 2 weeks of usage, I went back to typescript to build out something else and realized Go handling is way better. Then I learned Rust and everything else was history.
You can also mitigate the issue. I do Apex, and I have objects with embedded error handling. That sounds weird since it breaks the single responsibility principle, but it simplifies the code for the caller. You can chain methods using the same objects; each method knows to bail out if you provide an error. This means you can write your implementation and check for errors in one place, handle the error anywhere, and provide an error-free object to continue the chain. The API is mostly for noncoders who can't handle errors (they don't even think about it), so it has to be as robust as possible. And by avoiding throwing exceptions, it's made very explicit. The language doesn't support Generics; otherwise, I would have done a wrapper with overloaded methods accepting those wrappers.
The main issue with error handling in Go isn't syntax. It's that the types of errors a function can return are not part of the function signature. You have no idea what errors you're checking for. In a nontrivial program, most nontrivial functions end up propagating a union of unknown errors up the stack, which isn't much better than unchecked exceptions.
Go isn't Rust. "Errors as values" cannot be implemented well without union types. Having a special syntax for errors would: 1) Allow union types to be added for errors and not the entire language. 2) Go, unlike Rust, has recoverable panics, so the runtime is already maintaining a jump table. Having a special syntax for error handling would allow the happy path to have 0 runtime overhead. Go should adopt something like Zig's error handling.
100% agree with you. I just don't think that comparing Go's error handling to JavaScript isn't fair since JavaScript is 90% bad design and 10% incredible optimization
I pretty much never have to read actual stack traces in Go, because I'm hyper dilligent about wrapping _every error_ returned in `fmt.Errorf("some context: %w", err)`. It's genuinely jarring to go back to a Node environment and have to dig through a jumbled stack trace after writing Go and every error is telling me exactly what went wrong in plain english. I like to tell people, if you have to dig through an error's stack trace to know what went wrong and where, your error handling is wrong.
In principle I like that idea, but what would happen if many concurrent tasks print to stderr at the same time? Does Go have some nice way to handle that? It's a genuine question for a real problem. Rust has the tracing crate for that.
@@thehibbi fmt.Errorf isn't a print statement, despite looking like one. It's actually wrapping the error and is then typically returned to the caller. So the answer is, wherever you're actually logging errors will have the full stack for its specific error, meaning if you have a batch of errors, you'll see a "stack" for each error you logged individually. Also, stdlib added some other nice conveniences around the same time, allowing you to aggregate multiple errors into a single error and then be able to unwrap them alongside their individual traces. So you could choose to only log at the top most level even if somewhere in the chain you were aggregating multiple errors
Something we do at my job is if/else chaining related blocks, and I haven't really seen it done in other places. All the values are scoped to the entire if/else chain, and the only downside is sometimes function signatures can break the blocks if it has no error branch, but most things we write or interact with have error returns. Example (assuming foo and bar are int values or something): ``` if fooVal, err := foo(); err != nil { return 0, fmt.Errorf("foo failed: %w", err) } else if barVal, err := bar(); err != nil { return 0, fmt.Errorf("bar failed: %w", err) } else { return fooVal + barVal, nil } ```
If you aren't doing manual resource management - e.g. you have defer/with/using/RAII/Context - there's not many reasons to care which function call failed. Where I handle these differently in Go, I'd otherwise use the type to distinguish cases I can handle from those that I can't. Well done btw, well done 4:30
This obviously differs, but at my workplace we do very minimal error checking because even when functions can error, they shouldn't, because we control the input. The only places where we really check for errors is for network requests where failure is out of our control. If something breaks and we get an unexpected error then the appropriate response (for us) is to let that error bubble to the surface so we can fix it, not attempt to handle it which often results in suppressing bugs.
No such thing. You're pretty much relying on humans not making a mistake. The issue with the "bubble to the top" nonsense is that errors can get masked and transformed along the way. You also don't know how much damage it has done along the way. Handling errors is about soft landings when mistakes occur, or creating self healing systems. Remember, failing to prepare is preparing to fail.
@@chudchadanstud No, if someone makes a mistake in their input, that's still their input they're responsible for. Errors don't get "transformed along the way" and any error causes the system to stop processing the entire request so nothing is changed if an error occurs.
@@chudchadanstud " The issue with the "bubble to the top" nonsense is that errors can get masked and transformed along the way. " That seems to suggest a fundamental problem with how your overall system design approaches errors. There shouldn't be any opportunity for errors to be "transformed along the way" because you should only have error handlers at or near the top level in the first place. If you're trying to "handle" errors by wrapping a try catch around every function that can fail, you're working against how the scheme is intended to be used and setting yourself up for these kinds of problems. Disclaimer: I don't do JS/TS and my experience with exceptions comes from C++ where they actually make sense.
@@isodoubIet Bubbling to the top doesn't guarantee that your error won't be transformed. You're just throwing an exception and hoping someone else handles it, hoping that the system crashes or the process/request is killed unpredictability. You're also killing a process for a useless error. The Go styles ensures that your not placing your errors on hopes and dreams and that your function only throws errors in needs to throw. Consider that you press a stop button in a machine and during the clean-up operation a files fails to close. That is a trivial issue, your machine shouldn't crash or cut the power because a file failed to close. You can simply log the error and move on with the rest of the steps.
It definitely could be better though and it hurts the scan ability of the code as in it can make it harder to understand the non-error handling code flow. Still things could always be worse…
The 'idiomatic' way of error handling in Go is to try to align the happy path on the left - if you do that then you can configure your IDE to collapse 'if err != nil{ ... }' blocks so that you only see the non-error case. But TBH the happy path is almost always trivially simple - the more interesting and critical parts of your code will probably be the error handling
Oh men I totally agree with you because my code in typescript looks exactly like the example that you do with all those try catches for handle all possible errors, I realized that I'm using the wrong language
go's error handling has worse warts, like it being entirely untyped strings with no proper interface, we had this huge file full of string matching "things the net and net/http can throw" to status code and error message conversion in our reverse proxy, implementing basically all cloudflare error codes and some extra because we had this stupid SAP app as a customer that only logged status code and they had connection issues.
learn to love go error handling. It actually makes the code super easy to read, for the happy case you just read whatever is on the left side of the code (the editor often even just hides the return err lines). It also makes it super easy to test your functions and make them go into every error case..
The main point of try-catch approach, is that, in 99.9% of all webapps, or even on 70-80% of software, you do not care about which error is occured. If you look at the go code, which has, for example, stack trace of 6 inner function calls, in each of these function calls you probably just sending the error upwards. And this exactly why it sucks. Mostly, you just propogate the error up to the main function, and in this case, you try-catch just does it better, it does it automatically without you requiring to do redurant error checks. Another thing what is bothering me about go error handling - it is not restrictive, you can just ignore the error, and it will panic because of that
And no. Try-Catch does not work lile you said, the error is not merely propagated, the stack itself is *unwinded*, it's very, very different at a fundamental machine level. Simply propagating the error value as a return is jist another data transformation in your call graph and you can handle it with regular control flow.
Man, I'm just picking up Go as someone who's primarily coded in JS/TS and watching this makes me feel more sane. Up to this point I was just like "Wow, handling errors is such a gigantic pain in the ass." Go may not have ideal error handling, but it's so much less complicated than handling try/catch forks, especially in the lower layers of an app.
Catching exceptions all the time is nonsense. They're best used for unexpected errors. Return values, callbacks, wherever are much better for expected errors that need to be dealt with as part of normal control flow.
@@gileeeHi, we automate an erp with javascript (it's scripting languaje) his example is exactly how we deal with errors to do the proper rollbacks. When you care WHAT failed, you have to do that try catch hell
@@Slashx92This, as long as you are writing an application that does something you are going to want some sort of fault tolerance. What if the network goes down? What if the filesystem isn't available? What if there isn't enough ram to complete the operation? How do you fail gracefully? What happens to the logs? How do you recover? Do the users need to be prompted that their action failed?
union types might be possible. But for result type, it's certainly not. Imagine everyone have to update their codebase to change from (T, error) to Result[T]
I will fully admit that when I initially write JS for myself that I don't immediately write error-checking into my code. That being said, if I expect that code I'm writing is either going to be used by other people, some level of "robust" beyond "this is a thought experiment or some hacked up localhost toy project," or both, then you bet I do whatever I could to error-check (and hopefully error-proof) the living daylights out of it, including making sure that if I _intentionally_ add any throw statements in my code under the assumption that my code can be dropped into third-party projects that what I throw is reasonably meaningful and understandable (at the bare minimum a contextually proper error message). Granted, it would be easier if we didn't have to memorize or otherwise check MDN for what throws and what doesn't every time we wanted to localize our own error-checking, but I'm no stranger to having a language reference open in another tab or multiple tabs in another window. Which reminds me - I need to reinstall onto my dev computer the userscript I made in 2019 that puts links to MDN's JS reference as a toggleable sidebar.
As a certified Go fanboy that sometimes hates writing an insane amount of “if err!=nil”, this gave me an entirely new appreciation for my favorite language
i think it was Java that has "throws [some type]" for method signatures? so you know exactly what error is thrown and by what? seems like a decent solution, yeah its a bit boilerplate but at least theres a heads up.
this one I'm not with prime. Note that I also prefer return value based errors, but if the model is exceptions, than for the most cases, the error is forwarded to the client (we can discuss if the forward model is correct - vs wrapping the error, but that is other discussion. And in the go example, the err is also forwarded) assuming error forwarding is the model, exceptions minimize undefined behavior by nature. period. no one with experience would return null on the catch branch. It is simply wrong. that is for the client to choose, not the implementation I prefer return value based because i really like funcional pipelining and function composition. having a try/catch is a mess to compose (the exception behavior is like a non linear, hidden, return value). I like linear control flow. But having that control does make the happy path less clean. choices
Wow my error handling is so much more explicit now that I’ve rewritten everything in Go! *Turns around* glad there’s no super-easy-to-trigger primitive runtime construct that bypasses all of it.
Very good point. So when you don't need/want to handle the error, JS way is better/simpler than go. But when you need/want to handle the error, golang is actually better. Agreed. But, I would say that most of the time we are actually raising up the error to be handled higher up the stack (with perfectly fine error handling code). So most of the time golang's way would not be really "pleasant". For me the best selling point in favor of golang's error handling (which I do agree is much better then JS and almost any other language) is the *explicity* of it! Explicit is always better then implicit, IMHO. So it's generally a bit verbose and awkward I must admit but REALLY consistent, explicit and clear on intents. So it reads really really fast and natural to everyone. No misteries or hopes or technical theories about it. An this is why golang error handling is better then most
The only problem with Go's error-handling is that you're allowed to stuff the `err` result in a bit bucket and ignore it. I wish the compiler would require you to always check the `err` value.
I honestly started doing this in Python to some degree when I need more granular control over error handling. Sometimes, I want to handle different ValueErrors differently, and the doing so requires subclassing ValueError with a dozen or so different exception types and its just gets crazy. Way easier to return an error code and read the function’s documentation to figure out what that error code means.
I've got C++ at university programming classes (I'm studying maths but we do have some programming) and my professor just outright banned exceptions for low performance, weird ass implementation and being wack in general lol. The man is also a CTO of a company which develops software for finding petroleum sources in the ground, the project takes fuckin 20 minutes to build and it has no try catch anywhere in it
"my professor just outright banned exceptions for low performance, weird ass implementation and being wack in general lol." He's wrong on all three counts. Exceptions perform better on the happy path, the implementation is just fine, and they're very easy to understand.
@@isodoubIet Super agree. Especially in C++, where you can avoid writing try/catch in 99% of cases by just using RAII, exceptions are by far the best way. A bit sad that professors are teaching this nonsense when the C++ standard itself encourages the use of exceptions.
I just hate the insane level of repetition of writing if err != nil 500 times... They should have added syntactic sugar for this like what Rust has. Then it would be almost the perfect language. The other pain point is lack of generic containers like what Java has built-in (TreeMap, List, Set, Map, ordered maps and sets, doubly linked list, stack, queue, double ended queue etc.)
Yeah error handling in JS is terrible, which is why i made my own library to make the error handling closer to languages like Rust. Its so much nicer to handle errors as values instead.
1:29 In go you don't know which error has been returned as well. You know where from it was returned, but definitely not which one exactly. It could be fixed in Go by specifying concrete type of error, but the general rule is to specify just an`error` - the widest as possible. So, Go just forces a developer to propagate manually, like nodejs did with err-first callbacks.
if err != nil { return err; } is a snippet... and copilot is really good at just suggesting that, so it is not that tedious tbh. And having errors as values let's you do amazing things with how they should be handled, etcpp. I don't get when people say go-error-handling is bad, when in reality it's a great thing to have. It's like saying it is annoying that i have to update the type-definition in Typescript whenever I want to assign another property. Yeah, it's work to do, but it is a good thing that you have to do that.
The real problem with JavaScript is that they don't have the necessary language features to provide a good way to handle errors. They should honestly look at Swift's approach, which is very similar albeit much more powerful thanks to pattern matching, guards, and other language features that makes it a breeze to work with. Exception-based error handling is bad when the language doesn't provide ways other than try/catch to deal with error propagation. I'd say the same for Go. They should provide ways to invert the error handling, or make it implicit, like Rust does with the ? (previously try!) for Optionals and Result (as long as the return type is also Result/Option)
Also Prime's example of using try/catch is not equivalent to handling the error with != nil in Go. In Go the error propagation is explicit (return err), whereas in JavaScript and other languages that use exception-style error handling, they are implicit or "automatic". You can still handle the error in a function and then rethrow the error to propagate the error. It's just a separate control flow, which Go doesn't have, so it's not 1:1. The major drawback of JavaScript is that there's no way to mark a function as throwing. It's a lack of syntax problem, not a conceptual problem.
I'll like golang adopts vlang error handling, it is option and result but with simple syntax, I know vlang is knowed as vaporware, but the ideas behind its error handling are pretty good
I agree, but sometimes it's nice to just not care about handling specific errors and just put a try-catch block in the main function, specially during prototyping.
Love this a lot more than the typical 'reacting to videos' videos. Need like a playlist to be able to find these kinds of talks.
Yup!
to be fair, prime can turn a 5 minute video in a 40 minute one lmao
In js, most libraries(native ones as well) throws errors that you may not know about. Sometimes this errors aren't even the native Error type. In Golang you know exacly what kind of error you are expecting, I like that explicitly.
nice website 👌
libraries should specify what errors they should the throwing, if they don't it is a crappy library.
@@amotriuc Or the language is crappy if everyone can just ingore errors.
@@swattertroops-yaaa what about them? See Java /C# Languages they do exception handling quite well. Including those ones. Bad ones are C++ JavaScript.
@@mdbk2 it is much easier to ignore a return code then an exception. If you miss an exception it will be caught on top level so you know it did happen. if you ignore result code no1 will know it did happen.
I absolutely love Go error handling. Honestly I hated error handling when I started learning programming because I started with JS. When I started Go, I hated the error handling until one day after writing a service in it and realized how nice it is, looks bad but who cares? It works well. I agree though that Rust does error handling better but Go’s is super simple along with everything else about the language
this!
agreed - in the big picture, syntax is pretty trivial; what it enables you to do matters more
Though rust error handling does a bod job about source of error unless you set the proper env variable in debug. With Go and wrapping, you get better pinpointing. From what I recall, Zig has the best error handling - the best from both worlds.
Disagree completely. Writing exception handling in Go is awful, because there is so much code that is NEVER executed, but is just there "in case" ... Just yuck.
@@Tony-dp1rl what do you mean by code that is never executed? If you know that it is never executed, just use underscore to ignore the error, otherwise it is a branch worth handling.
Both Go and Rust use monadic error handling: Rust uses the Result monad while Go utilizes a monad known as "Writer" (which basically means a value with some other accumulated value, which in this case is the error). Rust is better in this sense because you cannot represent invalid states. On the other hand: in Go you could return `(nil, nil)` or both an error and a value. The result of this, is that error handling in Go feels more imperative and in Rust more declarative.
writer is when you append things to a list, hashmap, etc. (err, value) is just a pair, ie an anonymous product type. writers are useful outside of FP languages when you want to accumulate multiple errors or accumulate operations and execute in bulk or mock io.
Agree, add to this that the Go compiler does not encourage (force) you as a programmer to handle the error case, it can be happily ignored.
Whereas Rust compiler forces you to handle all cases making it impossible to access the return value (compile-time error) without checking for an error first. This is a huge win.
The `Result` type and pattern matching in Rust are simply awesome.
Then ironically when you use the return value from the JS function that could be a value or null, you'd still have to do an "if value === null" and handle that case too lol
What a stupid post upvoted by people who don’t use JS/TS.
Null is never a native output in JS. It doesn’t happen on its own. Some programmer simply decided to use the “null” object.
All empty references in JS are undefined.
Don’t blame the language because your coworker or your own stupid face decide to return “null” objects either due to a cargo cult believe that value is useful, or because they need to signify an assigned but unused value.
by the way, null is the only case when == is preferable over === since it also handles undefined. (*only* undefined + null)
aka x == null.
but null !== null
Having coded full time in js/ts for 5+ years and 3 months in go, I'm 100% convinced go's way is better. Never want to go back having build production apps in both. In go, I know every error has at least been acknowledged if not handled correctly.
Go doesn't force you to handle it. Just write a bunch of ugly IF err != Nill checks. Proper wrapped error types are cleaner and guaranteed they're handled.
That's why I said at least acknowledge it. It's up to all of us to handle errors smartly. But at least I always know if a go function can error.@@natescode
OP should not be anywhere near a computer
@@natescode Every languages have the way to write ugly error handling. It's just that TS never tell you if it gonna throw or not. Go at least shows explicitly that error is possible without using any LSP, and if someone write ugly check it's just gonna be very obvious.
@@OnFireByte It's not explicit when the majority of nontrivial functions in your program are propagating an unknown union of errors up the stack. The issue with error handling in Go is you don't know what errors are being returned. It's nothing like Result in Rust.
I like errors being part of the return type. My problem is that instead of using typed unions / associated value enums / algebraic data types, which is the correct, type safe way to deal with this, where accessing the value requires you to branch on having an error or not... they made it a tuple of nullable values.
I guess it's good for a simple language that doesn't want extremely basic and important features for modeling data like being able to represent mutually exclusive states. (I'm salty.)
That said, e.g. LLVM does errors like Go (with a custom wrapper return type) and it's really fun.
On the other hand, I also really like Elixir style error handling. Elixir is built to do incredibly fault tolerant distributed systems where lots of things can error. So the idea is don't handle individual errors; any time there's an error, just recover from a known good state.
If you've ever worked professionally in Go _and_ in another language without error-by-value, I strongly think that most people would prefer EBV. It significantly reduces the cognitive overhead when you need to jump into an unfamiliar service/repo. No surprises on where errors might pop up from.
I like Rust's Option and Result much more than Go's err. Usually (in code I write) if I'm handling an error I'm dealing with data from the code that may have encountered an error. A Result makes me decide how I want to deal with errors before I can get at the data (I can return the error with "?", panic on errors with ".unwrap()", convert to an Option with ".ok()", map Ok()/Err() values, do clever iterator stuff with arrays containing error values, and otherwise gain a lot of flexibility by having the error and data bundled together).
Before Go I did a bit of Delphi and C. I hated try-catch in Delphi, I didn't know where to put, I didn't know where exactly it came from and if you did not set it right initially it was even worse to find what is happening in production. C was such a breath of fresh in regards of errors - very simple and understandable, but really annoying to deal all the time with memory allocations and leaks so when I found Go and had the chance I switched all the codebase to Go in the next few years. This was 5-6 years ago, I still think it was the right call. I have colleagues that when from Delphi to C# and to this day we have this debate where they consider try/catch superior
I am primarily a C# developer. Exception < (value, Err) < Result, simple as that. The problem with exceptions is as soon as they are possible in a language, they can happen anywhere and everywhere, for any reason, without warning. They are awful for having certainty about how the program will behave.
Results are superior especially when the language has the ? or equivalent operator to pass the handling up the chain and otherwise enforces the handling of both cases, but I frequently try catch Exceptions in C# as early as possible to convert into a Result type just to get a little control back
If there is something I really love it's jumping into a new codebase and the previous developer decided to make a "resilient" and "error handling" application that just does err != nil checks everywhere but instead of actually handling it in a meaningful way just returns it up the stack. Now I suddenly get a error code somewhere but I have no idea where the error originally happened or what could have caused it because the stack is unwound and the program already exited... I really love to then start hunting down all places where the return err is used and add logging just to be able to try and catch it 😍
@@orterves I never liked Result, (value, error) feels less boilerplaty and more control. Result is just an abstraction of it
Absolutely right! The messed up part is, lot of Devs prefer smart looking or cleaner code over secure or more stable code! Btw I love go's error handling or generally, "Errors as value" over old school "Exceptions"
Old school would be to pass the error through arguments, or return an int with negative values, and is arguably worse; If you need the error message you then have to call `strerror(errno)`. Couldn't be anymore magic in my eyes.
The problem is not exceptions but the language's ability to provide proper ways to deal with them and convey information about them. A language should provide ways to mark functions and things like pattern matching, operators, guards, etc. to handle them. Not just try/catch, which is arguably the worst way to deal with exceptions.
@@dealloc try catch is what makes exceptions what they are. Maybe there is a different syntactic way to express this, but exceptions (in the context of things you "throw") will always have the undesirable behavior of being inherently different than other normal values. Its the ability to throw something up the callstack implicitly that (in my opinion) causes issues, not just the scoping issues with try catch blocks. Being able to throw an exception is essentially a goto statement without an explicit label. They can make the control flow of an application hard to understand and they inherently break locality of behavior. They are also harder for static analysis tools to reason about.
I will say that Java forcing explicit definitions of what exceptions may be thrown from a method at least gives more explicit information and helps with static analysis. But it still suffers from the non-locality of behavior issue. Having errors as values means you don't need separate constructs to handle errors. They can be handled like any other value in your system, which leaves a lot more flexibility in how you handle your errors. Even when exceptions are values (like being objects in Java), you still have to use special syntax to extract them and pass them around, and I've rarely seen that ever done. You essentially create two separate control flows for your system, one of which is implicit.
Although returning a tuple is wayyyy better than throwing and catching, I still find it really awkward to handle. I think Rust and functional programming languages got it right by using a Result wrapper type / monad. That way you are sure that is one or the other and it cannot be the error and the value at the same time, moreover, you get methods and functions to deal with error handling in a really logical and secure way.
Thanks a lot for the vid. I'm only starting with Go and I was quite confused that error handling in Go was criticized this much, as to me it looked fine after years with JS. Yes, annoying, but as someone said on the internet "at least it makes you pretend you thought about how to handle potential errors", which sounds like a win for me. Maybe I haven't yet written enough Go and I'll have a better idea of where this pattern falls short as I run into more cases of handling errors, but for now I'll move forward with confidence writing Go and not stress about that error handling is "ugly". Also, thanks to everyone who added perspective in the comments sections, I learned a lot while reading about nuances of this pattern!
Exactly. It is comparing apples to oranges. Equivalent Go code to "const val = await foo()" would be "val, _ := foo()". See, it is even shorter and easier to read :)
Aren't panics way worse to deal with than JavaScript errors?
@@zaneearldufour not if we talking about a panic in http handler with proper recovery middlewares
@@zaneearldufourhow exactly you get panic from val,_ := foo()? Or you think it won’t panic if you write val, err := foo()?
@@zaneearldufour You don't use panics for error handling typically, you use panics for situations where you can't keep on running your program and thus you panic and see the stack trace of where it panics. I.E when you don't set the port properly for a server (use letters for example) and you have to stop the server so that you correct that before you keep on going with running the program.
@@Gusto20000 I have not written Go (I do write Rust btw), so I don't know what's the value of `val` when err == nil. But isn't the only logical choice being that it must be nil? In that case, using val should result in some sort of panic shouldn't it?
As old-school C programmer, I can tell that this sort of error hanling was in the C forewer. You always return an error code and then you have an if statement to handle this error code. The Go haven't invented anything new here, except now you can separate an actual function response from the error that gives you this nice shugar.
The main problem with this error handling is precisely a mess of if or switch statements after each function. You need to handle every error and then pass it on to the caller by wrapping it into another error what happens (face it) in 80-90% of the cases.
But this extra code can be source of all sort of bugs because it is unnecessary operations that language forces you to do after every function call.
Not only that, but you cannot chain call the functions this way.
Lets say you have a function that return a string and another one that parses this string and returns integer. Because you must handle errors, you will need to process the first function response then call the second one and process its response as well. While you don't care if the first function didn't return a string or if the string wasn't a valid integer and the second function have failed, you will have to handle error twice and return a nil, err in both cases.
This is why exeptions in C++ and Java was so great, it eliminates the need of doing that. You can handle errors in one place, you don't need an if statement after every function.
There is no real problem to find out what function have failed, all you need is to know what type of error have happened and you can achive it by throwing different clases of exceptions. And thanks to inheritance you can group similar exceptions into groups by subclassing them from the some base class and handle this base class. It gives you way more flexibility on how to handle errors in your application and where to handle them.
I will not be surprised if exceptions will be introduced in go at some point.
Yeah but the annoying things is that if you want the error to bubble up, you still have to handle it
Which is one of the reasons why rust does a much better job, you can just use the question mark
yes, but you simply return the error
@@SandraWantsCoke That's handling, you write actual code that does that
At this point people just need to start implementing their own Maybe/Either/Result Monad, its not that hard at all, its actually insanely simple, Just a couple 5-6 functions and you're good to go.
Monads can be implemented in any language and they're insanely usefull (Just look at rust)
When you need errors to bubble up - you have to question your architecture.
One thing I did in my C# code was add an extension method to Task that just is a wrapper around a try catch that returns a (T, Exception?) tuple that lets me do this and wrap existing methods so that I never get exceptions
If you want to handle errors by value in javascript you can adopt that convention and return a tuple like in go. Then the code will look very similar to go. The code in this video is complex because you are translating error via exceptions into error via return value. If you use exceptions all the way through then in javascript you will just do .catch on your calls to foo and bar and do your handling in there which is imo a lot cleaner than go
thanks for hammering this home. I've been telling myself not to cut corners with error handling. I will reap the benefits of it later.
I think this moves onto another interesting point, exceptions are meant to be exceptional (or so the saying goes), while Go's error is par for the course.
Many developers use exceptions as an 'all hope is lost' - because if it wasn't that type of situation then it wouldn't be exceptional (stacktraces are expensive afterall).
I've seen many a function written that instead of throwing an exception, would return false, or an error object, as the problem that arose is expected and part of the potential flow.
From this video, I get the impression that an error is an error, no matter where it comes from in Go's paradigm.
```
var err error
if err = something(); err != nil {
return errors.Join(fmt.Errof("specifics %v", value), err)
}
var data Blabla
if data, err = other(); err != nil {
return errors.Join(fmt.Errorf("some specifics %v", value), err)
}
```
You did talked about this sir
That's how you create wonderful errors management in `go`
Either your effects are in your types (tuples, monads, a failure ADT) or they are in their own effect system (exceptions, signals)
I'm glad you mentioned a monadic approach. It comes down to the same tradeoff:
I want control -> handle it all in line
I want ease of expression -> abstract error handling from the "happy path" computation
Different tools for different jobs
Or use Rust and just get both?
To be fair, as someone who regularly grumbles about Go's error handling I'm not advocating for it to be more like JavaScript's with Try catches, I just want like a '?' operator that would be the equivalent of "if err != nil {panic(err)}" and another like "?{return err, ...}" that would be the equivalent of "if err != nil {return err}" or "if err != nil{return err, nil, etc.}". When I need to do some custom error handling, Go is the best, but when I'm just testing something out or I don't expect any errors that shouldn't be returned I'd rather not have to constantly copy and paste the same codeblock.
This is all I want as well. I dream about it
Not sure if '?' operator should panic
If you're just testing stuff out, why not use the placeholder character "_" ? Then you don't have to handle the err case.
? should not panic, it should propagate.
The problem with pure propagation is that you have leaky abstraction. If you treat each package as standalone OS library, it should propagate typed errors from its package, hence just using ? is not applicable in that package's implementation. Rust fails at that as well.
I would like tike have `.ErrOnFail(err, "%w: error creating database user: %w", ErrInvalidState, err)` which would propagate a wrapped error.
@@ja31ya exactly
I think it really depends what type of software you are building. For example: If you are building a basic CRUD Web API , I would guess +95% of the errors are "unrecoverable". Like if your database connection fails, what are you going to do? If you have bug in your business logic? If you failed to validate the request parameters correctly? For all of these cases there is no "recovering" from them. So having such fine grained error handling makes no sense. You can handle all of these with middleware (single try/catch).
It's bit different when you are building something where not-recovering isn't really an option, like an operating system, IOT related or something where there are no transactions to save you. It boils down to using the right tool for the right job.
Totally agree.
Errors in backend are unrecoverable for the most cases.
The only things can be done are: report or log errors, rollback DB transactions and show users sorry messages.
It's better to return meaningful messages for user-input-validation errors though.
I think this is pretty much true across the board, regardless of domain. I mean realistically, what strategies are even available to recover from most errors? Trying again?
@@isodoubIet Yeah, I guess "recovery" is not the correct word here. It's more like generic and non-generic error handling. Most errors can be handled in a generic way but I can imagine situations where application/tool has a lot of internal state and you have to essentially rollback the mutations manually when errors happen. I can see try/catch being really clumsy in those situations.
@@z0n_ I suspect this is not possible to in TS/JS so this might explain why so many in this audience have issues with exceptions, but in C++ you can design things so that everything rolls back automatically in the case of an exception.
When an exception is thrown, stacks will unwind and destructors will run. You then have several options for how to deal with rollbacks. You can divide your work in a prepare/commit fashion (so that all possible exceptions are thrown in the "prepare" stage, and all changes in system state happen in the commit stage), or you can have custom types that explicitly roll back changes when destroyed, unless explicitly dismissed. A third option, which may sometimes be possible, is to not rollback completely but to allow things to be in a valid but unspecified state after an exception is thrown.
Either way you're never rolling things back manually, which is pretty clunky no matter what error handling strategy is being used but surely unworkable in the presence of exceptions.
@@z0n_Or either use a transaction system or defer state modification to the final step of an operation where it can’t fail.
You can just create some wrap function
So that it returns undefined rather than throw error
What if 95% of errors in your app just need to be logged and corresponding http status code returned? It's more more convenient to let those errors bubble up to a centralized error handler instead of repeating err != nil checks all across the call stack.
The other 5% are usually retries/circuit brakers etc. which are handled by libs (talking about web services in this case)
this is the one for me, in most of the code i write, 95% of the time an error happens i just want to rollback the transaction, do some logging, and respond with 500, all of which go in a middleware. that way, i dont have to think about exceptions most of the time
100% agree.
I also would recommenced to use error monitoring services like Sentry along with logging, they make life a lot easier.
When I was introduced to Java and had like 6 months to start writing production grade code, almost 20 years ago, I was annoyed one had to try-catch all the time, but ohhhh boy!! Did that prepared me for future experiences with post-Modern hype driven development languages... It got drilled in to me that anything can fail at any time.
Thank God, I was waiting for that last "...agen". I thought you will not do it.
This totally make sense but is a bit weird though. I mean, the go-style error handling here is just what all language without try-catch simply do. C also do that, just without the tuple-like return syntax.
The mistake of the Go design is that Error is a separate thing that needs its own interface. Instead of treating "error" conditions as Errors, you can just treat them as normal conditions.
I learned how much this makes sense after now doing a golang code base, I love it so much more.
If err := doSomething(); err != nil {
Handle error
}
Is something I find beautiful. Wish it would work even if a value is returned.
The problem is actually because of the error, you can't use the result immediately in the same line, ever. Even simple things like string to int, getting a value from a map, etc.
I wish there were a single liner error handling with Go, though.
What language has single line error handling and what does it look like?
Kinda like "expect" in Rust?
Zig's try/catch are very cool, but the language has to treat errors as something special. In Go errors are simply some value that you return.
@@samfelton5009 C has more or less when you use the return value for errors.
if(err = f(n)) {...}
When err is a non-zero int, the if evaluates to true and you deal with the error. The "downside" is that you have to use pointers as out-parameters for setting normal results
@@samfelton5009 zig. In zig instead of "if err != nil return err" you have "try". The try in zig is not exception handling, it just does the go err != nil part automatically. For example:
try func();
instead of:
value, err := func()
if err != nil {
return err
}
that zig version is the exact same thing as the go version
The multiple try/catch with outside variables is exactly how I'm dealing with errors in critical processes of an erp we automate lmao. Shit's so ugly
Every time you speak nicely about Go I feel better about my life choices
I LOVE it personally, just deal with your error right away and be done!
My biggest issues with errors are not mine but library's. Hunting errors in huge libraries is quite tedious.
One of hardest pills I’ve ever had to swallow: code aesthetics do not matter nearly as much as code pragmatics.
go's error handling is neither pragmatic nor aesthetic.
Neither matter as much as expressing oneself clearly, though. When you drop a platitude using a made-up expression like "code pragmatics" which people can only guess at the meaning of, you already failed.
He touched on it right at the end. The really problem is not that it returns the error. Returning errors rather than throwing them makes them explicit and 100% better. The real issue with go is that "if" statements everywhere is such a shit way to then handle those exceptions.
Error handling should be built into the language as a first class citizen and languages like Scala that allow you to then pattern match and using things like map, flatMap, for comprehensions, etc are such a better way to handle things. Sometimes you don't need to care about every place it could fail. Sometimes you want to just write code that says do these things as long as they keep working and let me handle the failures at the points I care about. While is why using a Monad and gaining fluid support for chaining map operations is great.
There's a package called neverthrow that's quite cool, even though it's a bit awkward, it adds that extra layer of security to JS/TS. Takes the mysteries out of errors, and forces you to handle them. To me personally though, although I like the philosophy behind Go's error handling, my issue is that there's no shorthand for the err = ... / err != nil stuff. That pattern is just so common that I just feel it can be shorthanded to like an "or". Something along the lines of `someVal := someFunc() or { handle error }` and the compiler just forces you to handle the error.
I hated Go's err handling. Until I launched my first maybe small but serious app and there were no errors, fatals, boundary cases or anything like that. I just couldn't believe it!
After 15+ years in IT, one's used to getting unexpected errors, NPEs and similar snafus all the time, especially during first tries. But in Go... NOTHING. At first I thought I screwed up so badly, I missed some errors. But there is the point - it is impossible to miss errors in Go, I correctly managed all situations!
It was love from the second sight. ❤
Depending on the context, implicit error management can help you write simpler code, while explicit error management can help you write more robust applications.
I would argue that it is almost the same with garbage collector and RAII.
With garbage collector and/or RAII, you can write simpler code. But you are pushed/forced to ignore errors in destructors so your application is not as rigorous as it would be with explicit resource management.
Error handling is hard. We have automated coredump analysis and monitoring to make sure it cores in the usual places we don't bother to fix 😅
This days I tend to use a library like purify-ts to wrap my Promise codes in Javascript. I find it insane not to be able to reliably know what errors a function throws or even be forced to handle it. This however means that I'll have to be using creating more functions to handle errors (like .then promises handles)
Yeah, I prefer Rust's error handling over Go's.
hmm, Go error handling used to make me suicidal cuz it's a pattern I'm not used to. After 2 weeks of usage, I went back to typescript to build out something else and realized Go handling is way better. Then I learned Rust and everything else was history.
You can also mitigate the issue. I do Apex, and I have objects with embedded error handling. That sounds weird since it breaks the single responsibility principle, but it simplifies the code for the caller.
You can chain methods using the same objects; each method knows to bail out if you provide an error. This means you can write your implementation and check for errors in one place, handle the error anywhere, and provide an error-free object to continue the chain.
The API is mostly for noncoders who can't handle errors (they don't even think about it), so it has to be as robust as possible.
And by avoiding throwing exceptions, it's made very explicit.
The language doesn't support Generics; otherwise, I would have done a wrapper with overloaded methods accepting those wrappers.
The main issue with error handling in Go isn't syntax. It's that the types of errors a function can return are not part of the function signature. You have no idea what errors you're checking for.
In a nontrivial program, most nontrivial functions end up propagating a union of unknown errors up the stack, which isn't much better than unchecked exceptions.
Go isn't Rust. "Errors as values" cannot be implemented well without union types. Having a special syntax for errors would:
1) Allow union types to be added for errors and not the entire language.
2) Go, unlike Rust, has recoverable panics, so the runtime is already maintaining a jump table. Having a special syntax for error handling would allow the happy path to have 0 runtime overhead.
Go should adopt something like Zig's error handling.
@@aazzrwadrf Rust also has recoverable panics, they just as you pretty please not to use it. The machinery for it is there, though.
@@isodoubIetthe process is, but the thread is not recoverable. Therefore the runtime doesn't maintain a jump table to recover from panics.
100% agree with you. I just don't think that comparing Go's error handling to JavaScript isn't fair since JavaScript is 90% bad design and 10% incredible optimization
I love the take and I also raw dog my JSON.parse every single time unless I have more than on potential error source
There is this too:
```
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
err := ...
check(err)
value, err := ...
check(err)
}
```
I pretty much never have to read actual stack traces in Go, because I'm hyper dilligent about wrapping _every error_ returned in `fmt.Errorf("some context: %w", err)`. It's genuinely jarring to go back to a Node environment and have to dig through a jumbled stack trace after writing Go and every error is telling me exactly what went wrong in plain english.
I like to tell people, if you have to dig through an error's stack trace to know what went wrong and where, your error handling is wrong.
In principle I like that idea, but what would happen if many concurrent tasks print to stderr at the same time? Does Go have some nice way to handle that? It's a genuine question for a real problem. Rust has the tracing crate for that.
@@thehibbi fmt.Errorf isn't a print statement, despite looking like one. It's actually wrapping the error and is then typically returned to the caller. So the answer is, wherever you're actually logging errors will have the full stack for its specific error, meaning if you have a batch of errors, you'll see a "stack" for each error you logged individually. Also, stdlib added some other nice conveniences around the same time, allowing you to aggregate multiple errors into a single error and then be able to unwrap them alongside their individual traces. So you could choose to only log at the top most level even if somewhere in the chain you were aggregating multiple errors
@@superderpyderps ah that's good to know. Thanks for the explanation!
You can add .catch after the method
function foo() {
method1().catch(err=>/*handle it*/)
method2().catch(err=>/*handle it*/)
method3().catch(err=>/*handle it*/)
method4().catch(err=>/*handle it*/)
}
More like this
function foo() {
const value1 = method1().then(res=>res.json()).catch(err=>/*handle it*/)
const value2 = method2().then(res=>res.json()).catch(err=>/*handle it*/)
const value3 = method3().then(res=>res.json()).catch(err=>/*handle it*/)
const value4 = method4().then(res=>res.json()).catch(err=>/*handle it*/)
}
Something we do at my job is if/else chaining related blocks, and I haven't really seen it done in other places. All the values are scoped to the entire if/else chain, and the only downside is sometimes function signatures can break the blocks if it has no error branch, but most things we write or interact with have error returns. Example (assuming foo and bar are int values or something):
```
if fooVal, err := foo(); err != nil {
return 0, fmt.Errorf("foo failed: %w", err)
} else if barVal, err := bar(); err != nil {
return 0, fmt.Errorf("bar failed: %w", err)
} else {
return fooVal + barVal, nil
}
```
If you aren't doing manual resource management - e.g. you have defer/with/using/RAII/Context - there's not many reasons to care which function call failed. Where I handle these differently in Go, I'd otherwise use the type to distinguish cases I can handle from those that I can't.
Well done btw, well done 4:30
This obviously differs, but at my workplace we do very minimal error checking because even when functions can error, they shouldn't, because we control the input. The only places where we really check for errors is for network requests where failure is out of our control. If something breaks and we get an unexpected error then the appropriate response (for us) is to let that error bubble to the surface so we can fix it, not attempt to handle it which often results in suppressing bugs.
No such thing. You're pretty much relying on humans not making a mistake. The issue with the "bubble to the top" nonsense is that errors can get masked and transformed along the way. You also don't know how much damage it has done along the way.
Handling errors is about soft landings when mistakes occur, or creating self healing systems. Remember, failing to prepare is preparing to fail.
@@chudchadanstud No, if someone makes a mistake in their input, that's still their input they're responsible for. Errors don't get "transformed along the way" and any error causes the system to stop processing the entire request so nothing is changed if an error occurs.
@@chudchadanstud " The issue with the "bubble to the top" nonsense is that errors can get masked and transformed along the way. "
That seems to suggest a fundamental problem with how your overall system design approaches errors. There shouldn't be any opportunity for errors to be "transformed along the way" because you should only have error handlers at or near the top level in the first place. If you're trying to "handle" errors by wrapping a try catch around every function that can fail, you're working against how the scheme is intended to be used and setting yourself up for these kinds of problems.
Disclaimer: I don't do JS/TS and my experience with exceptions comes from C++ where they actually make sense.
@@isodoubIet Bubbling to the top doesn't guarantee that your error won't be transformed. You're just throwing an exception and hoping someone else handles it, hoping that the system crashes or the process/request is killed unpredictability. You're also killing a process for a useless error.
The Go styles ensures that your not placing your errors on hopes and dreams and that your function only throws errors in needs to throw.
Consider that you press a stop button in a machine and during the clean-up operation a files fails to close. That is a trivial issue, your machine shouldn't crash or cut the power because a file failed to close. You can simply log the error and move on with the rest of the steps.
@@gileee What of you have multiple erros that occur whilst your bubbling to the top. How do you catch those?
I really love this design choice in golang
It definitely could be better though and it hurts the scan ability of the code as in it can make it harder to understand the non-error handling code flow. Still things could always be worse…
The 'idiomatic' way of error handling in Go is to try to align the happy path on the left - if you do that then you can configure your IDE to collapse 'if err != nil{ ... }' blocks so that you only see the non-error case. But TBH the happy path is almost always trivially simple - the more interesting and critical parts of your code will probably be the error handling
Oh men I totally agree with you because my code in typescript looks exactly like the example that you do with all those try catches for handle all possible errors, I realized that I'm using the wrong language
Just don't use exceptions at all
rust error handling is the best error handling I have ever used with result set and question mark operator 👍
go's error handling has worse warts, like it being entirely untyped strings with no proper interface, we had this huge file full of string matching "things the net and net/http can throw" to status code and error message conversion in our reverse proxy, implementing basically all cloudflare error codes and some extra because we had this stupid SAP app as a customer that only logged status code and they had connection issues.
Yes, I much prefer error enums like Zig/Odin/Rust. Go's biggest weakness is not having real enums and unions
learn to love go error handling.
It actually makes the code super easy to read, for the happy case you just read whatever is on the left side of the code (the editor often even just hides the return err lines).
It also makes it super easy to test your functions and make them go into every error case..
With zig you can use try and avoid all that boilerplate.
Exactly
this is the kind of content i loved this channel for
The main point of try-catch approach, is that, in 99.9% of all webapps, or even on 70-80% of software, you do not care about which error is occured.
If you look at the go code, which has, for example, stack trace of 6 inner function calls, in each of these function calls you probably just sending the error upwards. And this exactly why it sucks. Mostly, you just propogate the error up to the main function, and in this case, you try-catch just does it better, it does it automatically without you requiring to do redurant error checks.
Another thing what is bothering me about go error handling - it is not restrictive, you can just ignore the error, and it will panic because of that
ignoring error states is possible in any language, being able to explicitly not care about the error state is totally fine in some cases.
And no. Try-Catch does not work lile you said, the error is not merely propagated, the stack itself is *unwinded*, it's very, very different at a fundamental machine level. Simply propagating the error value as a return is jist another data transformation in your call graph and you can handle it with regular control flow.
@@marcs9451The stack is not necessarily unwound. It is in most languages but that's because the language designers were lazy.
@@marcs9451 What's the difference? isn't "unwinding the stack" the same as propagating up the stack?
@@gagagero Lol what would have been the "non-lazy" choice?
Man, I'm just picking up Go as someone who's primarily coded in JS/TS and watching this makes me feel more sane. Up to this point I was just like "Wow, handling errors is such a gigantic pain in the ass." Go may not have ideal error handling, but it's so much less complicated than handling try/catch forks, especially in the lower layers of an app.
Writing code while recreating/solving problem while also talking. That sounds like a native speaker of a computer language.
why is no one talking about how he just ignored .catch?
yeah you can use .then and still return your value with only one .catch
Catching exceptions all the time is nonsense. They're best used for unexpected errors. Return values, callbacks, wherever are much better for expected errors that need to be dealt with as part of normal control flow.
True. No one actually writes js code like he did in this example.
@@gileeeHi, we automate an erp with javascript (it's scripting languaje) his example is exactly how we deal with errors to do the proper rollbacks. When you care WHAT failed, you have to do that try catch hell
@@Slashx92This, as long as you are writing an application that does something you are going to want some sort of fault tolerance. What if the network goes down? What if the filesystem isn't available? What if there isn't enough ram to complete the operation? How do you fail gracefully? What happens to the logs? How do you recover? Do the users need to be prompted that their action failed?
Will golang ever go the 'Result' path?
I'm missing that and union types in GO
union types might be possible. But for result type, it's certainly not. Imagine everyone have to update their codebase to change from (T, error) to Result[T]
@@OnFireBytenot everyone has to do that - they're compatible
Love these kinds of videos, I didn't get the controversy until now
You have that JS option in golang too, the magic underscore :D
I will fully admit that when I initially write JS for myself that I don't immediately write error-checking into my code. That being said, if I expect that code I'm writing is either going to be used by other people, some level of "robust" beyond "this is a thought experiment or some hacked up localhost toy project," or both, then you bet I do whatever I could to error-check (and hopefully error-proof) the living daylights out of it, including making sure that if I _intentionally_ add any throw statements in my code under the assumption that my code can be dropped into third-party projects that what I throw is reasonably meaningful and understandable (at the bare minimum a contextually proper error message). Granted, it would be easier if we didn't have to memorize or otherwise check MDN for what throws and what doesn't every time we wanted to localize our own error-checking, but I'm no stranger to having a language reference open in another tab or multiple tabs in another window. Which reminds me - I need to reinstall onto my dev computer the userscript I made in 2019 that puts links to MDN's JS reference as a toggleable sidebar.
As a certified Go fanboy that sometimes hates writing an insane amount of “if err!=nil”, this gave me an entirely new appreciation for my favorite language
i think it was Java that has "throws [some type]" for method signatures? so you know exactly what error is thrown and by what? seems like a decent solution, yeah its a bit boilerplate but at least theres a heads up.
It could have been a lint at least. Typescript's had this proposal for a long time, yet it hasn't been merged.
Typescript NEEEDS to add a throws modifier
this one I'm not with prime. Note that I also prefer return value based errors, but if the model is exceptions, than for the most cases, the error is forwarded to the client (we can discuss if the forward model is correct - vs wrapping the error, but that is other discussion. And in the go example, the err is also forwarded)
assuming error forwarding is the model, exceptions minimize undefined behavior by nature. period. no one with experience would return null on the catch branch. It is simply wrong. that is for the client to choose, not the implementation
I prefer return value based because i really like funcional pipelining and function composition. having a try/catch is a mess to compose (the exception behavior is like a non linear, hidden, return value). I like linear control flow. But having that control does make the happy path less clean. choices
Unpopular opinion the best way to do this is the way they do it in C# and using expressions in the catch
I always loathed exceptions it’s a sneaky hidden control flow.
Wow my error handling is so much more explicit now that I’ve rewritten everything in Go! *Turns around* glad there’s no super-easy-to-trigger primitive runtime construct that bypasses all of it.
Very good point. So when you don't need/want to handle the error, JS way is better/simpler than go. But when you need/want to handle the error, golang is actually better. Agreed. But, I would say that most of the time we are actually raising up the error to be handled higher up the stack (with perfectly fine error handling code). So most of the time golang's way would not be really "pleasant".
For me the best selling point in favor of golang's error handling (which I do agree is much better then JS and almost any other language) is the *explicity* of it! Explicit is always better then implicit, IMHO. So it's generally a bit verbose and awkward I must admit but REALLY consistent, explicit and clear on intents. So it reads really really fast and natural to everyone. No misteries or hopes or technical theories about it. An this is why golang error handling is better then most
No Joke. Why would I handle a potential error from JSON.parse? What am I supposed to do with the JSON.parse error?
no one really knows lol
The only problem with Go's error-handling is that you're allowed to stuff the `err` result in a bit bucket and ignore it. I wish the compiler would require you to always check the `err` value.
I honestly started doing this in Python to some degree when I need more granular control over error handling. Sometimes, I want to handle different ValueErrors differently, and the doing so requires subclassing ValueError with a dozen or so different exception types and its just gets crazy. Way easier to return an error code and read the function’s documentation to figure out what that error code means.
Why not create a single subclass which has the error code as an attribute?
With the js example you can wrap them in an allSettled
please wine for this gentleman
I've got C++ at university programming classes (I'm studying maths but we do have some programming) and my professor just outright banned exceptions for low performance, weird ass implementation and being wack in general lol. The man is also a CTO of a company which develops software for finding petroleum sources in the ground, the project takes fuckin 20 minutes to build and it has no try catch anywhere in it
And he's right. Status checking like in go is better for perf and maintenance. He will probably tell you to use std::expected now that it's there
"my professor just outright banned exceptions for low performance, weird ass implementation and being wack in general lol."
He's wrong on all three counts. Exceptions perform better on the happy path, the implementation is just fine, and they're very easy to understand.
@@isodoubIet Super agree. Especially in C++, where you can avoid writing try/catch in 99% of cases by just using RAII, exceptions are by far the best way. A bit sad that professors are teaching this nonsense when the C++ standard itself encourages the use of exceptions.
@@Tuniwutzi Yeah I can see why someone in a space-constrained environment might want to avoid exceptions, but the rest is largely just FUD.
Well, equivalent would be not return Promise, but throwing error anyway (even after special handling)
I just hate the insane level of repetition of writing if err != nil 500 times... They should have added syntactic sugar for this like what Rust has. Then it would be almost the perfect language. The other pain point is lack of generic containers like what Java has built-in (TreeMap, List, Set, Map, ordered maps and sets, doubly linked list, stack, queue, double ended queue etc.)
love these kind of videos
Absolutely fantastic apples to apples and oranges comparison.
I think youd be interested in ginger bills article on why odin will never have exceptions
prime already did an interview with bill but I think the article would be a great read
Yeah error handling in JS is terrible, which is why i made my own library to make the error handling closer to languages like Rust. Its so much nicer to handle errors as values instead.
1:29 In go you don't know which error has been returned as well. You know where from it was returned, but definitely not which one exactly.
It could be fixed in Go by specifying concrete type of error, but the general rule is to specify just an`error` - the widest as possible.
So, Go just forces a developer to propagate manually, like nodejs did with err-first callbacks.
Preach
Never really thought of this. Good content
if err != nil { return err; } is a snippet... and copilot is really good at just suggesting that, so it is not that tedious tbh. And having errors as values let's you do amazing things with how they should be handled, etcpp. I don't get when people say go-error-handling is bad, when in reality it's a great thing to have. It's like saying it is annoying that i have to update the type-definition in Typescript whenever I want to assign another property. Yeah, it's work to do, but it is a good thing that you have to do that.
Catching someone else’s error is modern GOTO
The real problem with JavaScript is that they don't have the necessary language features to provide a good way to handle errors. They should honestly look at Swift's approach, which is very similar albeit much more powerful thanks to pattern matching, guards, and other language features that makes it a breeze to work with.
Exception-based error handling is bad when the language doesn't provide ways other than try/catch to deal with error propagation.
I'd say the same for Go. They should provide ways to invert the error handling, or make it implicit, like Rust does with the ? (previously try!) for Optionals and Result (as long as the return type is also Result/Option)
Also Prime's example of using try/catch is not equivalent to handling the error with != nil in Go. In Go the error propagation is explicit (return err), whereas in JavaScript and other languages that use exception-style error handling, they are implicit or "automatic". You can still handle the error in a function and then rethrow the error to propagate the error. It's just a separate control flow, which Go doesn't have, so it's not 1:1.
The major drawback of JavaScript is that there's no way to mark a function as throwing. It's a lack of syntax problem, not a conceptual problem.
Zig imo has the perfect solution to this. If go implemented it it would still feel gomatic but be much cleaner
I'll like golang adopts vlang error handling, it is option and result but with simple syntax, I know vlang is knowed as vaporware, but the ideas behind its error handling are pretty good
Good thoughts thanks
I agree, but sometimes it's nice to just not care about handling specific errors and just put a try-catch block in the main function, specially during prototyping.