anyhow explicitly isn't designed for what you are trying to do here. It's designed to be the last link in the chain (and complementary to thiserror, not in competition). If you are using anyhow any deeper than your top-level binary crate, you are likely to be in for an unpleasant time.
Now that we have agentic coding I just write everything in Rust and couldn’t be happier. The struggle with rust was writing it, go was made so it was easy to write for mid level engineers. Now that we have agentic coding I’m not sure Go’s value prop holds up anymore
My rust services have been nothing short of amazing from a performance and reliability perspective
They treat it like it's JavaScript, falling back to using String/&str needlessly instead of making new types. They do ugly `static Mutex<Refcell<` a-la global JS variables for info sharing instead of working out the lifetimes to do it properly. It loves making functions infallible and then panic-ing within them and certainly I wouldn't use them for unsafe at all - they hallucinate safety comments which are in fact, totally unsound.
Of course these are all surmountable with an experienced developer to regularly step in and unfuck the code, but forcing them into 'harder' territory where every problem is not solved by a .clone() and an Arc<Mutex<>> means they will spend minutes 'thinking' about basic lifetime issues until I step in and add the missing `move` in a closure.
"Write an SQL Repository with this interface"
Sweet - no need for SQLc or an ORM
What?
I'm not sold on Rust being a great language to use with AI unless the reason to use it is a lot more than just Rust being fashionable.
The verbose error handling diluting the interesting parts is one thing, but the main issue is the weak type system. Having to read the callee's code to check if it deviates from `res xor err`, or if it mutates its arguments. Figuring out which interface that `func (o *Obj) ()` is implementing, if any. Dealing with documentation that is a wall of 100 disappointing oneliners all repeating the function name.
Rust is information-dense and takes longer to master, but it's not inherently cryptic, there's a finite amount of things to know. Memory management sometimes take a bit of thought to write, but it's straightforward to review, you can trust it's correct if it compiles, you just keep an eye out for optimizations.
In my opinion these problems originate in architectural style. Much of the open source written today is designed to impress the audience instead of focusing on the problem.
Compared to Rust, Go as a language requires a lot more effort to review. You have to be on the lookout for basic gotchas like not checking if a pointer is nil, placing `defer` in the wrong place, using a result when err isn't nil, and so on. Plus, diffs are messier because unused variables are a compilation error, and _, err := can change into _, err = solely due to new lines above.
Absolutely insane syntax choice in a language where everything returns 2 values. At least do var:, err: =
If the LLM gives you safe code you know there are entire classes of things you don't have to review for.
That said, I agree with you. My experience is that LLMs are great if you are highly competent in the domain in which you let them work. And it's probably easier to be competent in Go than in Rust.
Aah, I am sure the chickens of vibe coded origin, will never come to roost.
The usual reaction or opinion from e.g. good C++ programmers switching to Rust is that the added guardrails and expressivity are great and make things easier.
Go is too verbose and the type system isn't expressive enough. Rust code is littered with little memory management details and it requires tons of third party libraries.
I think coding agents will eventually be able to get the low level details right on their own. Reviewers should be able to focus on architecture, design and logic mistakes.
I also think we need a high level formal specification language to tell agents what we expect them to do.
Let’s make that specification Turing complete while at it.
Jokes aside, IMO it will be a good natural progression. Specify the problem statement in LLM specification, generate the code in Go/Rust whatever is the language of your choice and review the generated code to make sure it adheres to the architecture/design principles that you have set.
It doesn't have to be a new language. I'm sure some existing language can be used to create a DSL that serves this purpose.
It can obviously never be complete. Some parts of the spec will always have to be natural language if we want to make the best use of LLMs.
If only there was an entire class of well-studied languages which don't have any such ambiguity. They'd be perfect for programming LLMs! We could call them "programming languages" perhaps.
For me, one of the bigger complaints is that Rust isn't pedantic enough. Panic free Rust isn't taken seriously enough as an idea.
I wish it would catch even more things, since it works so well.
you need to know the conventions to spot what's not there (did you miss the error handling? or the magic comment for the whatever codegen serializer? c'est la vie!)
edit: just a few comments below an even better description of what I'm trying to convey: https://news.ycombinator.com/item?id=48264853
The magic comment stuff is very much “do it once” and it’s done (for example if using go generate).
In practice, anything that makes it easier for humans to program also makes it easier for LLMs to program.
You also wont typically learn that the LLM is close to the limits of understanding your code base until after it has blown past it's own capabilities, leaving you with a mountain of code that you are not skilled enough to fix.
Java, C# are good choices as they tend to enforce a certain structure. Go, good because it's very readable even if you dont know the language.
C++, Rust are poor choices unless you are already a senior in that language.
I don't think the value prop has changed at all there. One day the AI gravy train will stop and people who used AI to punch above their weight will no longer be able to debug the stuff they built unless they put in the hard work of learning the language.
Nothing to worry about with Go in that respect because of how much it's been designed to be simple. Even the annoying err/nil checks you need to do all the time are in service of that simplicity. It gets old fast but it leaves nothing to the imagination.
Agents seem to have a better time with Go. Humans need to review the agents outputs and in general they have an easier time to do it with Go.
Rust has practically one error, it's the Error trait. The things you've listed are some common ways to use it, but you're entirely fine with just Box<dyn Error> (which is basically what anyhow::Error is) and similar.
An easy rule before you make a knowledge based choice is Thiserror for libraries, helping you create the standard library error types and Anyhow for applications, easy strings you bubble up.
Or just go with anyhow until you find a need for something else.
* Someone tells me to use enums for errors, in a comment like yours
* I try writing the enums by hand, implementing the error trait
* I realize that in order to use the ? operator I need to implement From on my errors (I’ve read so many comments about how awfully verbose Go errors are, so I assume I’m supposed to use ? in Rust). There are also some other traits IIRC but I’ve forgotten them.
* I realize that this is pretty tedious, manual work, so someone points me to thiserr or similar
* Now I’m debugging macro expansion errors and spending approximately the same amount of time
* I ask around and someone tells me not to bother with thiserr and to just write the boilerplate myself or else to use anyhow or boxed errors everywhere
* I try using boxed errors everywhere, which works, but now I have all of these allocations which feels like I’m doing something that will bite me later. Oh well, but now I need to annotate my errors so I can figure out what is actually happening. I guess I should use anyhow for this?
* Anyhow mostly works but this is approximately as verbose as the Go error handling that I’m told is Very Bad, and when I ask for code review most Rust people are telling me not to use anyhow because errors should be enums, at least in the API surface
I’m sure I’m doing it wrong, but as with many things in Rust, the Right Way is so rarely clear and every other Rust person gives different advice about how to solve my problem and the only thing they seem to agree on is that Rust has an easy solution and that I’m following the wrong advice. (Similarly when I had lifetime problems and half the community told me to just use clone and Rc everywhere until I had performance problems, so instead I just had different static analysis problems).
I don’t love Go’s error handling. It feels like there has to be something better than its runtime-typing. But it largely gets out of the way—creating an error is just implementing the Error method, and if you need a concrete type you use Is/As/AsType. Wrapping is fmt.Errorf. All of this is built into the stdlib and used pretty ubiquitously across the ecosystem—I don’t run into “this dependency uses a different error framework”. Error handling is marginally more verbose than with Rust if you are actually attaching context in both, and neither solves the problem of which call frame attaches the context about specific function parameters (e.g., which level of error context specifies that the function was called with path “/foo/bar.baz”). It’s terrible, but it works—feels like the least bad thing until the Rust community can arrive at some consensus and document it in The Book. Or maybe I just need to try again in the LLM era?
Common in HTTP land. The HTTP system returns a different error type than the network I/O system, but they can be sorted out.[1]
[1] https://github.com/John-Nagle/maptools/blob/main/rust/src/co...
(there's a lot of this in Scala too, because of the various monads/containers, eg. the built-in Future, and then Scalaz.IO, FS2, Cats, ZIO, etc...)
regarding lifetime and performance problems, the best practice seems to design the rough scaffolding of the program first, with the structs, so the who owns whom can be figured out. but this is far from trivial. Rust is very good at forcing developers to stare at these problems, but solving them requires practice and patience.
for me the tech toolbox that makes sense is TS by default (because of the super convenient type system and tooling), and Rust when the circumstances really justify it (latency, throughput, scalability, cost effectiveness, or a need for a single native executable [though nowadays this is also pretty simple with Deno], or more safety/control [no GC])
The example on the docs page is quite clear:
https://docs.rs/thiserror/latest/thiserror/#example
Including all kinds of errors: Strings, tagged unions and automatically converting from std::io::Error with added context.
That one page document is the entire documentation for the thiserror crate.
1. thiserror just does codegen of the "standard" enum things people do. if you find debugging thiserror difficult, just write out the enums manually. sure it's uglier, but (roughly) equivalent. so its preferable as synctatic sugar for enums, but doesn't have any technical benefits (in the same way that syntatic sugar never really does).
2. for boxed errors, you only get allocations on your error path. Hopefully this is a cold path so it shouldn't matter.
There is a general theme behind rust error handling though which it can be good to internalize. In particular, the more details of your errors you encode in the type system, the more powerful things are. Any error type could just be
pub struct MyError(String)
the issue is that this gives very little information to a caller on what to do with your error. If you have no callers (e.g. are making a binary) it's fine, and (roughly) what `anyhow` does.
That all being said, when designing errors a natural question to ask is "can my caller do anything meaningful with this error"? For example, in Rust stdlib, `Vec::push` can allocate. This allocation can fail, which panics. "Proper" error handling would use the fallible allocation API, and propagating an OOM error or whatever through results. For most applications, this is not an error that is worth investing that much time into guarding against, so using the (potentially panicing) `Vector::push` makes things easier.
You can take this same perspective in other settings as well, in particular separating out errors into
1. structured data, that a caller should be able to extract/process to handle the error, and 2. unstructured data, that is more used for logging, and you expect the caller to pass up the call stack without inspecting themself.
Handling both types of these errors with `thiserror` can be tedious for little benefit. I've found it useful to instead solely use `thiserror` for category 1, and category 2 does other things. This could be using `.expect(...)`. There are some crates that make this nicer (e.g. `error_stack`). But the point is that it can significantly clean things up if you only encode in your error enums failures that you expect someone to handle, rather than just e.g. log.
This does somewhat validate your point that the "right way" I've been experimenting with (and mentioned above) is not just "use this-error".
Also: a big issue with `thiserror` is the tedium of handling the large error enums (or giving up on using it "properly" and shoving together multiple error variants in some unstructured error type). that is somewhat better in the LLM era, as you can have the LLM handle the tedium.
> Now I’m debugging macro expansion errors and spending approximately the same amount of time
This never happens once you've learned the language a bit more. Anyhow and thiserror are a cinch.
> I realize that this is pretty tedious, manual work, so someone points me to thiserr or similar
Claude writes Rust so effectively. It can do all of this for you now. It's effortless. In fact, I don't see any reason to use any other language unless I'm targeting web or some specific platform, or dealing with legacy code. Rust is now the best tool for most problems.
> Similarly when I had lifetime problems and half the community told me to just use clone and Rc everywhere until I had performance problems, so instead I just had different static analysis problems
Do this for a month, then it'll click and be second nature. Also Claude will make quick work of it now.
> feels like the least bad thing until the Rust community can arrive at some consensus and document it in The Book
It's difficult because it's so different. But once you get used to it, you'll realize it's the best approach we have right now.
> Or maybe I just need to try again in the LLM era?
Seriously this. You'll be writing Rust code as quickly as you would Python code. It'll be high quality. And the type system will mean that Claude emits better code on average. You'll pick it up quickly.
> And the type system will mean that Claude emits better code on average.
I’m curious if this is true. I believe that it emits better code than with a dynamically typed language, but as with people I don’t know that the sweet spot is at the extreme. Or maybe it is at the extreme when the context is small but as the context grows perhaps code quality suffers as it has more constraints to balance?
I'm so sorry for this btw.
The problems are trivial once you've used Rust for n hours, for some value n. It's just that these folks forgot the learning and headache they went through.
You're going to build that same recognition and familiarity using Claude over time. It'll seep in pretty quick, I'd imagine.
> I’m curious if this is true.
Being forced to emit an Option<T> or Result<T,E> and then having to actually use syntax to get at the goods forces the code to deal with errors the appropriate way, clearly, idiomatically, and typically in a good flow that is amenable to readability and easy refactoring. Other languages without Option, Result, and sum types baked into the language so fundamentally do not have this advantage.
I feel it every time I have to work in a TypeScript codebase, for instance. It's a strongly typed language, and can emulate sum types via discriminated unions. But that doesn't convey the same advantages because it doesn't enforce anything. It's far too lose to have the same advantages Rust has.
I think you'll feel the same way as you use the language more and more.
You'd describe it as a tagged union in some languages. So when you say you'd return an error with extra information, what that information is is associated with the specific variant of the enum.
Using yuriks AllocError as an example, if the error is SizeTooLarge, it has the size field. Other errors may have no additional data, others may have different data.
When you return an error from your allocating function, it's a known size, the size of the largest enum variant + the discriminant (tag).
enum AllocError {
SizeTooLarge { size: usize },
// etc.
}
This enum has a known size and doesn't require any dynamic allocations. enum MyApiBindingCrateError {
// You didn't provide an
// API key. Maybe we should
// design our interface to
// make this impossible
ApiKeyMissing,
// Client was unauthorized
// to make this request
AuthorizationError,
// The entity you requested
// did not exist (404'd)
NotFoundError,
// You're sending too many
// requests to the server
TooManyRequests,
// That specific error with
// the API
// Maybe users can't delete
// folders until they're empty
// Whatever
SpecificApiIssue1,
// Some other specific error
// with the API
SpecificApiError2,
// Server didn't respond the
// way we expected.
// Here's what it told us
UnexpectedHttpResponse {
// HTTP status code
status_code: StatusCode,
// If it had a
// string-encoded body
body: Option<String>,
},
// Unhandled Issue with IO
IoError(io::Error),
// Unhandled Issue with
// request library
ReqwestError(reqwest::Error),
}
The beauty with Rust is that you can create really detailed concrete errors at the crate level. Your callers will know exactly what the actual error states are.Your application can be a little less structured if you want. Though with LLMs, I'm using anyhow and thiserror a lot less.
but for most libraries I on allocation failure I don't expect any fancy logging system. maybe even panic is fine.
Stack traces are only useful for errors that indicate a bug in the program, i.e. something a programmers has to respond to. It's not useful for the vast class of bugs that are a result of wrong input, wrong external state, or infrastructure issues.
Rust projects tend to favor panicking over error handling for programmer bugs (which does indeed give you a stack trace depending on environment variables), or even better encoding the invariants in the type system, but there are cases where an error coming from a library are truly, actually unexpected, so both `anyhow` and `thiserror` do provide support for attaching a stack trace in those situations.
You can't have a stack trace on an error in the error path that failed to allocate. If you have a "jumbo sized" error and the error fails to allocate, it won't get reported. The only reporting you will get is that the error failed to allocate and this new allocation error overrides the error that failed to allocate.
If you need to handle an allocation error in the error path, then the error reporting path must abort, which means that the allocation error must be bubbled up.
There is no real solution to an allocation error inside the error path. Even if you preallocate an arena for errors, the error might be large enough that it won't fit inside the arena.
Hence the best thing you can do from that point onwards is to have an error enum with an AllocError variant that doesn't allocate. Said error won't contain any information beyond line numbers of the allocation error since you just don't have the space for it.
In the end you will basically end up with panic free code, but the error still bubbles up like regular unwinding.
So yeah you can do it, and I will do it in the future, but I personally think that the people who think this is some huge deal breaker don't understand the problem in the first place.
The minus side of Go is too simplistic GC. When latency spikes hit, there are little options to address them besides painful rewrite.
For the original issue of GC pauses, a narrow change is to move problem data to non-pointer-carrying types, or the bigger hammer of manually managed slices of those types. The second helps with fragmentation too. Some workloads can be split into multiple processes as a direct way to have smaller heaps. If none of those options are enough then off-heap storage lets you do whatever you want.
I do have some complaints about Go, but one of the big ones has been fixed since I last wrote much Go code and it seems like a fine choice for a lot of applications.
Interestingly, Rust has quite good failed compilation speed. That's almost good enough. The usual Rust experience is that it's hard to get things to compile, and then they work the first time.
To other people's usage patterns though, I imagine the group of people who don't do much with the type system rely more on running a built binary to see if it worked, which means they'll pay the full compile/link time cost more often.
Or having Cranelift as default backend.
https://uptrace.dev/blog/golang-memory-arena
These are all tools. Java used to have this all the time, and we (ex-java programmer) had ways around this until the JVM improved.
There are some fine points to the O(heap size), for example it's clearly unnecessary for the GC to scan objects that do not themselves contain pointers, and work is somewhat proportional to the total number of objects. Combining numerous small objects into manually managed slices, coming up with ways to make the most numerous items pointer-free, etc.
I learned a bit about this when an analytics workload I had ended up with unacceptable pauses (I think over 1 second), Go's GC is more sophisticated now but I think in any GC runtime you have the same considerations to some degree. Some of the best writing at the time was by Gil Tene, one of the principal authors of the C4 concurrent collector at Azul Systems, starting point here:
https://groups.google.com/g/golang-dev/c/GvA0DaCI2BU/m/SmEel...
Yes but Rust has a lot more availability of libraries to do stuff as a result. Want to do anything ML or scientific? You at least have a route in Rust where you don’t with Go.
As for availability if CGO is ok, then calling C or C++ code from Go is not that hard. Also, there is always an option to just start C++ process if extra data copies are OK.
There are various libraries people use for auth, etc. But rolling your own isn't hard - Go has (e.g.) bcrypt in the standard library, so most of the heavy lifting is already done, you can write a solid auth implementation in <50 lines of code using that.
Generally Go prefers libraries to frameworks. Wrap the hard bits up into a library that can then be used widely in any implementation, rather than rolling it into a one-size-fits-all implementation that doesn't really suit anyone properly.
“we” are all different and i can tell you from experience that there are also many people and teams who use go and prefer ORMs and frameworks and do not build everything from scratch …
This is typical Go culture. If it is not readily available in the language or the standard library, it's evil. It's an easy cop out to explain away the gaps in the ecosystem.
Not long ago, the Go team was saying that generics are evil for that very same reason.
My GitHub is dominated by rust projects, and I think it's the nicest overall language. But not nice enough to write bespoke solutions for problems that have had robust solutions since before I started programming! There is a basic set of functionality most web apps use, and that hasn't changed in a decade+; I don't want to re-write my own version of this, nor fight compatibility problems from (comparatively) poorly-integrated and documented libs.
I am trying to make good decisions, and am weighing "This long-standing solution does everything I need, and is easy to use and well-documented etc" vs "People on the internet are telling me I don't need it, or I can use X rust lib instead". It feels like the "We have McDonald's at home" meme.
I think few people would want to use an ORM for the stuff you use Go for, but there are things like SQLC which can generate a lot of your "dynamic DB magic" without actually being a real dependency. You can set SQLC up to run in a container in a completely isolated environment, and then use the output, but you can frankly also just maintain the SQL which frankly isn't that different than using an ORM once you've set up the automation with ridicilously strict policies.
We use Go for some of our more vital backend parts. We mainly use Python for entirely different reasons, but since we're an energy company it's nice to have a standard library that can do everything without any sort of external dependencies. It's not because we have some sort of "not invented here" fetish, it's because we have to write and maintain a literal fuckton of complaince documents for every external dependency we use and it's already a full time job for just for Python in our information security department.
It's just a different philosophy, but it's really not unlike Rails users importing Devise or Sideqik or RSpec.
lmao, basically, yes. except when you bring this up ppl think it's not a big deal / a means for self-expression. having to sort through which libraries you prefer to glue together is a kind of freedom, if you squint hard enough.
pub async fn dataset_stats_handler(
Path(dataset_id): Path<String>,
Query(verbose): Query<bool>,
) -> impl IntoResponse {
...
}
With a route like: .route("/datasets/{dataset_id}/stats", get(dataset_stats_handler))
…the "dataset_id" path variable is parsed straight into the dataset_id arg, and a query string "verbose" is parsed into a boolean. Super convenient compared to Go, and you type validation along with it.Many other things to like: The absence of context.Context, the fact that handlers can just return the response data, etc.
What I don't like: Async.
type DatasetStatsQuery struct {
Verbose bool `form:"verbose"`
}
func DatasetStatsHandler(c *gin.Context) {
datasetID := c.Param("dataset_id")
var query DatasetStatsQuery
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// query.Verbose == bool
}}
This is actually a great example - what happens in that Rust version when the input parsing fails? Go makes it explicit.I would assume Axum returns a bad request error for you when query parsing fails, but if you do want more control over how the error is handled, you can change the parameter type to Result<Query<bool>, QueryRejection>, and the type system itself documents precisely what errors you can match against.[0]
[0]: https://docs.rs/axum/latest/axum/extract/rejection/enum.Quer...
func Login(req LoginRequest, cookies Cookies, db *sql.DB) (LoginResponse, error) {
...
}
router.HandleFunc("POST /signup", fw.Wrap(Login))
It's just a wrapper.It also serializes/deserializes responses and handles both JSON and templates.
db is just a singleton-lifetime dependency, we often also have ctx, http.Request, http.Response, Cookie, which are request-time lifetimes.
I thought about open-sourcing it but most Golang developers seem to hate it with a passion, so I just gave up, haha.
“Anyhow” just allows you to conveniently say “some Error” if you don’t care to write out an API contract specifying types of errors your function might spit out.
If I care about the specific variants of error that a function can return, so I can do different things depending on what kind of error occurred, I'll read the docs and match. That's not really a "framework" thing; that's just a basic thing that anyone has to do in any language in order to consume an API. If I need to propagate the error, I'll do so (either directly, or by wrapping it in a variant of my own error type). I don't see how any of this is "framework"-y.
A crate's decision to use thiserror (or not) does not matter to me. If a crate exposes `anyhow::Error`, that's a lazy choice and bad API design, but still "works" and I generally don't need to care about it.
Or is there something else you meant when you said "error frameworks"?
Writing primarily applications, I couldn't tell you what error handling frameworks my dependencies are using: I literally don't know, and haven't needed to know in order to display, fail, or succeed.
EDIT to add: I use anyhow for this, so I should also add "add context to an error when I fall" to the list of things I do.
I am on team Io Error [on std rust]", somewhat arbitrarily. If I call a lib that is on Team Anyhow, or Team Custom Error Enum, I will have to do some (Straightfoward, but a little clumsy) conversions if I want ? to work. This is complicated by being able to impl From<ErrorType1> for ErrorType2 only in one direction if you don't control the other crate. (due to the orphan rule)
EDIT: Which I assume all my dependencies have done, given that anyhow is able to consume all of them.
I specifically called out writing applications as my use case: my only objection to tptacek's note is the somewhat universal "in practice". The burden for designing errors for a library that others will use is higher, but that's far from the default/universal experience.
Many more people are going to consume libraries & not produce any of their own, and I think my experience is representative there.
I mean the error is supposed to be tailored to the audience - I guess what you are saying is that you handle the error by saying "I called foo with X, Y, Z, and got this error back" in the logs - which your caller then also does - producing a log message of
ERROR: I called Foo with X Y and Z and got error: Die MF die
followed by
ERROR: I called Bar with X Y Z and a and got error: ERROR: I called Foo with X Y and Z and got error: Die MF die mf (still fool)
And so on and so forth.
If the counter is - don't log, that's fine, but you have to know where in the call graph that error state was reported to the logs
I haven't found any satisfying solution to it all; collecting information for logging vs information that a caller would want... I've been meaning to investigate tracing_error to see if it brings it all together.
edit: I've just finished debugging a multi system chain - FE -> SNS -> SQS -> Lambda -> DynamoDB -> Lambda -> Webhook -> My poor code
My code has multiple layers - and I was trying to find where in the very long chain of calls the data was being mangled
It turned out that there was an unlogged error, which was mismanaged by a caller - there's no shade here - the caller was handling the error how it was designed to, but by not logging that there was an error - it took a minute to understand.
Though go certainly did a much better job than rust on the standard library front.
People always tout this as a huge reason for not wanting a too big std in Rust (or "too useful" either), but IMHO that's just talking about reaching theoretical optimals, while leaving the community for years without good guidance via providing a opinionated practical and pragmatic way of doing things. Which I find to be a very unhelpful stance for a tool such as a programming language.
If a design of some std package didn't pass the test of time, and a new iteration would be beneficial, the language can leave its original API version right there, and evolve with a v2, with an improved and better thought out API after learning from the mistakes of v1.
Prime example: "hey we found that math/rand had some flaws, so here is math/rand/v2". A practical solution, and zero dramas as a result of having rand be part of std.
I definitely don't think stdlibs should be changed often, but it seems fairly damaging to a language when things may be added to a stdlib but never removed, no matter how broken or misconceived (see C++).
Rust is a great language, but the poor stdlib + overreliance on crates + explosion of unvetted transient dependencies makes it a hard sell for a lot of projects.
Having too much external code, like npm or rust crates, seems like a nightmare for me.
The language design makes sense in the context of Oberon (1987), and Limbo (1995).
Now when there are so many options finally building on top of Standard ML, and Lisp heritage, having to settle with Go feels like a downgrade.
I code since 1986, if I wanted if boilerplate error handling, or having cost as the only mechanism to declare constant values, there have been plenty of options.
in rust say a function returns Result<T, E> so either the we get a result all an error how is that different from (int, err) in go?
do you not still need to handle the error?
in go you just return the error up to whatever the top caller is.
Missing error handling is checked at compile-time in Rust (lint-time in Go), and can be enabled for any struct or function (https://doc.rust-lang.org/reference/attributes/diagnostics.h...), not just `Result<T,E>`.
Returning an error to the caller in Rust can be done with a single character.
At this point, I can't imagine a scenario not to use Rust for writing a web API.
But personally, I don’t mind Go at all. I’ve even begun to prefer it for some things. That may be Stockholm syndrome, though.
Not quite true. The unifying error trait is std::error::Error.
> pain when you have to pass them upward through a chain of calls
Kind of? You just make an enum with the various variants that need to be passed through and use the #[from] macro to generate the conversion code automatically.
It’s more characters than eg. A union type in Python or TypeScript, but it’s not much more.
Plus, it makes you think about your error design, which is important!
The entire point in Rust is that you wrap Error impls with other Error impls, or translate one impl into another using a match. I've found this is far more flexible and verifiable than most other languages, because if you craft your error types with enough rigor, you can basically have a complete semantic backtrace without the overhead of a real backtrace.
I use thiserror a lot to help with my impls. Notably, all it does is impl Display and Error. It's not a specific other paradigm because it basically compiles out, it's just a macro.
Anyhow is perhaps the closest one to another paradigm because it allows you to discard typed information in favor of just the string messages, but it still integrates well with Errors (and is one).
It's easy to write code that trivially eats memory. Plus any resources spent on it, are resources not spent on other cloud provider things.
Now there is a cult of rewriting everything in Rust. System level software? Yes. Web? I prefer not to.
Generally speaking there has to be a mechanism for optional handling of return values, in Go you can ignore everything (ew), you can use placeholders `_`, or you can explicitly handle things - my preference.
If you say "Well in C you have to handle the returns - I am not across C enough to comment, but I will ask you - Does C actually force you, or does it allow you to say "ok I will put some variables in to catch the returns, but I will never actually use those variables" - because that's very much the same as Go with the placeholder approach
edit: I am told the following is possible in C
trySomething(); // Assumes that the author of trySomething has not annotated the function as a `nodiscard`
(void)trySomething(); // Casts the return(s) to void, telling the compiler to ignore the non-handling
int dummy = trySomething(); // assign to a variable that's never used again
I welcome correction