I implemented Rust's unwrap() in Go, feedback asked

I started coding in Go a few months ago and now that I’m starting to get comfy with the language I compiled some utility functions and uploaded them to Github. One of them is sort of a reimplementation of Rust’s unwrap() for more convenient error handling. Here are the docs with all the explanation and examples: g package - github.com/n-mou/yagul/g - Go Packages but long story short:

import (
    "os"
    "github.com/n-mou/yagul/itertools"
)

// This:
fileInfo := g.Unwrap(os.Stat("."))

// Is equivalent to this:
fileInfo, err := os.Stat(".")
if err != nil {
    panic(err)
}

I’m looking for feedback from gophers. I know you shouldn’t panic every time a function returns an error. I usually propagate the errors if I’m writing a library or handle them separatedly if I’m writing a program, but sometimes panicking is the way to go (or panicking after handling the reasonable expected errors). And for those cases, Rust implements the unwrap() function for results but in Go there’s no workaround, you need to repeat the same 3 lines (if err != nil { panic(err) }) to exhaustion. This is all the syntax sugar I can write in a language like Go (lacking macros).

Am I the only one who finds this useful? And if that’s not the case, is Unwrap the best name? In Rust it makes sense because it unwraps a result, but in Go the term “unwrapping” is used in errors (and theres a function in the errors package called Unwrap) so it should be avoided but I can’t think of a better name (just Force but is already used by other functions in the same module). What do you think?

Thanks in advance

That’s not a good way to handle it, in fact you’re just stuffing the error into the context and not solving the case where the exception is being caught. (Do you want to panic when you encounter an error?) If so, I suggest you don’t deal with it that way. )
Looks like you just want the result (xxx:=), not (xxx,err:=)

I’ve met more than one person who thinks this way, and actually after I learn golang and then look at other languages, like kotlin or whatever, that use try-catch to handle errors, which is a bit like golang’s panic-recover.
But actually, the two are not the same thing, and if you handle golang errors in this way, then I would advise you not to do that.
Golang’s simplistic logic may seem cumbersome for this kind of error handling, but because of this, developers are able to grasp every situation that generates an error, and it’s more intuitive, which I really like.
The right thing to do with error is, don’t ignore it (unless you know what you’re doing). If you can use error, don’t panic. The impact level of panic is very high, and despite having defer to handle the next steps, it’s not great development practice.
If you really hate this approach: 1. Use other development languages, and 2. Use the code completion of the dot IDE.

I already said in my post that I don’t use it all the time. I know errors must be handled separatedly or propagated to be handled elsewere, but sometimes a function returns an error that the app shouldn’t handle (eg: os.Mkdir returns an error when there’s not enough memory, lack of user privileges or hardware error, neither of those cases would be handled by a user space app). This is not meant to sistematically quit the app any time an error is encountered and I do handle errors separatedly, but sometimes the app is not meant to handle some kind of errors and should stop if it receives them (like the example I mentioned above).

I’m not upset about errors as a return value, it’s a similar concept than Rust’s results and are handled similarly (in Rust you use unwrap_or_else() and a block where you pattern match the error type to handle specific cases but there’s also the unwrap() function that panics if an error is retuned, not to sistematically ignore every error but to handle these specific cases).

I don’t know what you’re looking for, I looked at your implementation code, and I think I understand what you’re thinking.
I will only use this type of code in test code, and as I said before, I will not panic in the case of error, so I will not use such code in the official code.
In the case of panic, it is much more intuitive to write directly than to package one layer.

1 Like

That’s right, I use it most of the times for faster prototyping (in a full fledged app almost all errors should be handled after all). This is just a syntactic sugar for what I’ve seen in Rust (a language that handles errors in a similar way) and I while experimenting with the language I thought to bring it to Go. I just want to know what other people who are more experienced in Go think about, to se if it’s only me or someone else also could find it useful. Thanks for your feedback.

Rust and Go do not have similar approaches in errors handling. Their base similarity is errors as values and that’s it. There are lots of discussions inside the golang community on how can we get rid of err != nil, but it’s another topic to discuss. Here, Imho, if the third party library panics, it’s not a good library. I’d agree that the only place where I can see its usage is testing, with a huge maybe. E.g. everything I’m working on with go requires graceful shutdown in any case, that’s why even std returns errors to be properly handled, not crashing with panic at runtime. And believe me, writing a recovery is even more tiresome than do nil check in place.

1 Like

Of course, one should never panic in a 3rd party library, and this experiment is not advocating for that. I mean, this is if err != nil { panic(err) } in one line, and should only be used when you would use panic(), which I agree is in a very few limited cases.

About Go and Rust error handling, I know they’re not identical, I said similar, and I with similar I meant error as return value instead of breaking the program flow with try/catch blocks. I understand that Rust was designed later than Go and the designers could tweak the formula (eg: the falible function doesn’t return a value and error but a type union to force the users to handle the error because there were cases in Go where the error was ignored instead of being handled properly). However, Rust despite being designed to handle the result and not ignore the error, it also gives the unwrap function, so, when there’s a rare case where the program should panic, there’s an ergonomic way of doing it.

I’m also aware of the error handling discussion and I understand the cumbersomeness (Go was designed to be a simple language with the minimum necessary abstractions and syntax features, the opposite of Rust, although with the addition of generics and iterators it’s slightly deviating from that original vision…). I personally wouldn’t change much of it, I find error as value return far better than try/catch blocks (I come from Java and Python). However, just a one-liner for panicking and another for returning ther error (some sort of “propagate” keyword that, when executed in a function, if the last variable of type error in the stack frame is not nil, stop the functon execution and return the error with the zero value of the type that function returns with the error) would be far more convenient. If Go supported metaprogramming I’d write a macro that does the same (a one-line equivalent to if err != nil { return zeroVal, err }) but, again, Go is meant to be simple by design, and metaprogramming is the opposite of simple.

Despite all of the above, I know I’m new to the language and I need to learn a lot of things before contributing to a discussion of language design. I like Go and it has a huge amount of advatages, many of them derived from it’s simplicity, so I use it despite having this mild inconveniences. The experiment was just to test if I could make it’s usage a bit smoother.

1 Like

On the other hand, @lemarkar. maybe I’m missing something so, Please, Can you advice me about the real case where I used panic in an application??

It was a file management tool for backups (a program, not a library to be used by others), and when I was creating directories I had to check if the directory already existed (in which case, os.MkdirAll would not return an error) and if not, create it. When investigating what type of errors can MkdirAll return I found those cases (lack of privileges, problems with memory storage and issues with external or network drives at a lower level). This utility was not supposed to handle those errors, so I panicked because I didn’t see a huge difference between panicking and printing the error with the stack trace and exiting with code 1.

Is there a reason I ignore were it would be preferable to do this:

if err != nil {
    fmt.Println(err.Error())
    os.Exit(1)
}

Than this:

if err != nil {
   panic(err)
}

Thank you for your feedback.

Well, it’s more about the preferences, habits and the way someone prefer their workflow to go. If I do something in main func, which can return an error, I use log.Fatal*, it has Exit(1) under the hood. But this thing fails as soon as I add defers with clean-ups or any other graceful shutdown related stuff. If you panic or through an exit, defer will not be triggered. Thus, I prefer to control my runtime. Your example is more about handling errors outside of your scope. If simple Mkdir returns an error, it comes from underlaying OS and even in this case I don’t see any reasoning to panic instead of handling this error as any other.

Oh, so that’s the difference. I didn’t knew panic didn’t trigger the cleanups. I’ll definitely change it to log.Fatal(). I chose panic overl log.Fatal() because it also printed the stack trace. How can I print it before calling log.Fatal()?

log.Fatal* will not trigger defers too. If you have any of them, they’ll be skipped. I don’t use trace, since I wrap errors with extended description where they’ve appeared and can find the place in code without a problem. In case you want to have trace you can try custom error types, which can hold additional information e.g.,

type myError struct {
    trace string
    err   error
}

func (m *myError) Error() string {
    return fmt.Sprintf("%s\n%s\n", m.trace, m.err)
}

You can also reduce errors in main, by moving them somewhere else e.g.,

func main() {
    // Set-ups and no error stuff.

    if err := run(); err != nil {
        // There are no defers in main and cleanup was already
        // finished, so it is possible just to crash it.
        log.Fatalln(err)
    }

    // Do something else if there were no errors.
}

func run() error {
    defer cleanup() // do cleanup here.

    // main logic goes here...
    if err := doSomething(); err != nil {
          return fmt.Errorf("error in doSomething: %w", err)
    }

    // ...

    return nil
}

Mmmm interesting. I also wrap my errors but I can’t rely on every 3rd party library I use also doing it, that’s why I wanted to print the stack trace. So, to ensure all cleanup code has run before exiting I can’t exit the program from any other function than main()…Thanks for everything and I’ll think about it.

Just adding to this a tad. There’s a Go Proverb that is part of the wiki called Don’t Panic:

See https://go.dev/doc/effective_go#errors. Don’t use panic for normal error handling. Use error and multiple return values.

And from effective go:

For this purpose, there is a built-in function panic that in effect creates a run-time error that will stop the program (but see the next section). The function takes a single argument of arbitrary type—often a string—to be printed as the program dies. It’s also a way to indicate that something impossible has happened, such as exiting an infinite loop.

This is only an example but real library functions should avoid panic .

They do note that it might be appropriate to panic during initialization:

This is only an example but real library functions should avoid panic. If the problem can be masked or worked around, it’s always better to let things continue to run rather than taking down the whole program. One possible counterexample is during initialization: if the library truly cannot set itself up, it might be reasonable to panic, so to speak.

var user = os.Getenv("USER")

func init() {
   if user == "" {
       panic("no value for $USER")
   }
}

But I probably wouldn’t want an external library to panic for me. App init is by definition a one-time thing so I like to be intentional about how I handle errors there (because sometimes things ARE recoverable). The good news is: your function at least is very explicit about the fact that it panics.

Most of the time I have seen example code where they are ignoring errors and just bailing on a problem, it’s called something like Must. So something like g.Must(os.Stat(".")). Unwrap specifically might be confusing because, as you noted, that term has meaning in the realm of error handling. When I saw the title of this post I assumed it had something to do with unwrapping errors.

If you’re interested, there’s some discussion here that might be relevant:

https://www.reddit.com/r/golang/comments/6v07ij/copypasting_if_err_nil_return_err_everywhere/

In summary: it’s pretty natural for people coming from other languages to dislike Go’s error handling. Many of us got used to it and now prefer it.

EDIT: I meant to reply to the main thread not your comment, @lemarkar.

3 Likes

I know. I never panic when writing a library and when I’m writing an application I only do it when there’s an unexpected error or an error the app is not supposed to handle. Panic signals the process that launched this app that there’s been an unrecoverable error and the program exited in a non-OK status. However I didn’t think experimenting with a syntactic sugar function was so prone to misleadingly make everyone believe I’m promoting or advocating for reckless error management (in fact, if someone wanted to ignore errors, they wouldn’t use that function, just val, _ := function() instead which is already a one-line), but if the first impression of everyone is to alert about this bad habits it means I’m not transmitting well my intention with this library.

Oh, you’re right, Must is a much better candidate. And it’s also go-like since there are functions in the standard library that return an error that have variants with the Must prefix that panic instead. If I continue to use this module (just for prototyping or very special cases, NEVER on a library) I’ll change it’s name and place a clear warning in the docs of not using it when creating libraries nor when they haven’t handled all recoverable errors. I think the confusion is that because I’m distributing a library, there’s literally a function in a library that panics. But it’s not a function that does some functionality and instead of returning an error it panics (which I agree is an awful practice), is a function whose only purpose is to panic when the user writing the app deliberately and explicitly wants to do so, the only difference is that with this function you can panic in one line instead of 3.

About the Go’s best practices. I knew you shouldn’t panic by default and that the idea of error as return value is to make you aware that this function may fail and you should be prepared to handle those errors (you still can ignore it by setting the error variable to _ though). But I didn’t think using panic was such a cardinal sin. I mean, I’m talking about an irrecoverable error that either the program failed to see it would happen or an error the app is not supposed to deal with (like the example of os.Mkdir). If it’s a bad practice to panic even when your are writing an app and encounter an error that you can’t handle and you want to exit the program notifying an abnormal state, What is the best pratice?

In this particular case, where what you want to do is exit the program and notify an abnormal state what is the best practice instead of panic?

I’ve been using Go for months and I’m used to them. And I don’t think they’re as awful as people think (I’ve spent far more hours debugging try/catch exception blocks in Java). I know I must be prepared, learn what type of errors can this function return and how to handle each case. It’s just that I think (maybe incorrectly) that some errors are not for the program to handle (like the lack of admin privileges), if I’m writing a function and encounter that error, return it and let the main package handle it, but if I’m in the main package and if have this error, I think the right thing to do is notifying it in stderr and exit, I can’t do literally anything else about it, there no “supermain” package where I can “return” the error.

The simplest distinction is that the tool library should not panic, the application (the difference is whether it will be referenced externally) and the initialization process (such as init) can be properly panicked (for example, the business of your program is to write files, but the files cannot be opened, as an application tool, then panic is acceptable). )
All logic serves the business, this is my philosophy. If it affects the main business, then I don’t hesitate to stop to avoid causing meaningless work (e.g., the web service can’t even listen to sockets) (but if it’s just a route exception, it shouldn’t stop the whole service)

1 Like

I see. We both agree in the limited cases where panic it’s acceptable but the problem is that I’ve implemented a library that panics for the user. If that function was supposed to be called for another different purpose and, as an adverse effect it panicked instead of returning the error I agree it would be unacceptable. But in this case is a function whose only use case is to panic, and in the docs and readme it’s explicitly said that this function is meant to be used as a syntactic sugar for panicking.

However I understand the way it was presented was misleading and I’ll add to the docs a warning about how this function was thought of a help for prototyping and that in production code panic is almost never a good choice. Thank you all for the feedback.