| ▲ | the_gipsy a day ago |
| There are several shortcomings with go's error handling. The author heavily lies onto rust, so the alternative is not exceptions but a `Result<T, Error>` sum type. No stacktraces and error wrapping forces you to not only invent unique error messages. You must also conceive a unique wrapping message at every call-site so that you can grep the error message and approximate a stacktrace. The weird "return tuple" , which obviously just exists for errors because there is not a single other place where you can use tuples in the language, and the awkward variable initialization rules, make it so that you use the wrong `err` var at some point. E.g. if you want to reassign the result to an existing var, suddenly you have to declare `var err error`, and if `err` already exists then you have to reuse it. There should be an enum type in go, or instead of the bizarre "return tuple" mechanics exclusive for errors, they should have added a better syntax sugar for errors like rust's `?` sugar. Instead we have something extremely tedious and quite error prone. > it has a lot of build in opinions which protect you from yourself It does have opinions, but too often they seem to be there to protect the language from being criticized. Sadly, this works, as marketing (lying) is an important factor towards making a PL popular in today's market. |
|
| ▲ | masklinn a day ago | parent | next [-] |
| > The weird "return tuple" , which obviously just exists for errors because there is not a single other place where you can use tuples in the language MRV, go does not have tuples. Go is not the only language with MRV (as a special case) and they’re not necessarily bad, iirc Common Lisp uses them as auxiliary data channels and has a whole host of functions to manipulate and refit them. Go is singularly incompetent at MRVs though, in the sense that only syntax and builtin functions get access to variable arity (e.g. if you access a map entry you can either get one return which is a value, or two which are the value and whether the key is/was in the map). So MRVs mostly end up being worse tuples infecting everything (e.g. iterators needing Iter and Iter2 because you can’t just yield tuples to for loops). |
| |
| ▲ | robocat a day ago | parent | next [-] | | > MRV Acronym for Multiple Return Values https://gobyexample.com/multiple-return-values | |
| ▲ | pwdisswordfishz a day ago | parent | prev [-] | | > MRV, go does not have tuples. > MRVs mostly end up being worse tuples I think you noticed yourself that you’re getting too hung up on terminology. Multiple return values are a half-hearted, non-reified version of tuples. | | |
| ▲ | masklinn a day ago | parent | next [-] | | No, MRVs can actually offer useful properties and features, that is what they do in Common Lisp. That Go does not do that has nothing to do with MRVs. | | |
| ▲ | erik_seaberg 16 hours ago | parent [-] | | f(g(), h()) can't work with MRVs, only f(g()). multiple-value-list, multiple-value-call, and quietly ignoring unneeded values makes them much more usable. |
| |
| ▲ | biorach a day ago | parent | prev [-] | | Which is what they said. I'm not sure what point you're making |
|
|
|
| ▲ | the_gipsy a day ago | parent | prev | next [-] |
| I forgot: The tenet "accept interfaces, return structs" is violated all over by returning the `error` interface. IMO it's okay to make behaviour-exceptions specifically for error handling. Rust for example doesn't really have builtin behaviour exceptions specifically for errors, they're generic to sumtypes and just happen to work well for errors. But then in practice you must resort to thiserror or anyhow helper crates to deal with errors in anything but tiny programs. If you do make behaviour exceptions for error handling, be honest and upfront about it. Don't say "errors are just values so just use them like regular vars" in docs, if then there are several huge exceptions (tuple-returns and breaking a core tenet). If you make exceptions then you might as well do them right, instead of half-assing them. I believe zig does it right, but I haven't gotten around to try it. |
| |
| ▲ | Seb-C a day ago | parent | next [-] | | "Do what I say, not what I do" is almost a design guideline of Go at this point, there are so many inconsistencies like this in the language design. | |
| ▲ | 9rx a day ago | parent | prev [-] | | > The tenet "accept interfaces, return structs" is violated all over by returning the `error` interface. To be fair, that expression came from a blogger who often wrote about Go. It is not a tenet held by the Go project. In fact, Rob Pike has made clear that he considers it to be misguided advice. It is only violated in the same way returning Result in Rust violates the assertion I made up for this comment: Do not return Result. | | |
| ▲ | the_gipsy a day ago | parent [-] | | https://go.dev/wiki/CodeReviewComments#interfaces | | |
| ▲ | wbl a day ago | parent [-] | | How on earth is that violated by error? error is implemented by types all over the standard library and beyond and consumed by functions that wrap errors in the errors package. It's exactly an example of what you claim is violated. Even more obviously your link isn't talking about functions but packages. There are some violations out there but generally when included packages define interfaces they are ones that get consumed like in io or it's dissolution. | | |
| ▲ | the_gipsy a day ago | parent [-] | | > The implementing package should return concrete (usually pointer or struct) types It's in the first paragraph. It goes on in the second: > Do not define interfaces on the implementor side of an API “for mocking”; instead, design the API so that it can be tested using the public API of the real implementation. And yes, io.Reader/Writer violate that too, because either the tenet is wrong or the design of interfaces in go is wrong. > Even more obviously your link isn't talking about functions but packages. It doesn't matter: your exporting errors behind the error interface, not your concrete error implementation. If you're just using one of the many ways to create stringly errors (!) like fmt.Errorf, you maybe don't notice but you are in fact returning interfaces all the time. | | |
| ▲ | 9rx a day ago | parent | next [-] | | The actual first paragraph states: > This page collects common comments made during reviews of Go code, so that a single detailed explanation can be referred to by shorthands. This is a laundry list of common style issues, not a comprehensive style guide. The document does not assert that you must not return interfaces or that it is incorrect to return interfaces. It only indicates that returning interfaces at inappropriate times has been a recurring issue found during code review. Sometimes returning an interface truly is the right choice, but when it isn't... Like most adages in programming, the aforementioned tenet holds validity in many cases, but, as always, "use your noggin" applies. | |
| ▲ | wbl a day ago | parent | prev [-] | | It does matter that packages and functions are different. It also matters what the io package actually does. https://pkg.go.dev/io . The io package has a very limited number of functions that return Readers. The vast majority of its functions take Readers or Writers as arguments and do useful things with them: e.g. Copy or LimitedReader. Most of the interfaces it defines (ReedSeeker, ReadWriteSeeker, etc) aren't instantiated by anything in it. os implements Reader and Writer for filehandles. net does the same for sockets etc. | | |
| ▲ | the_gipsy 21 hours ago | parent [-] | | It doesn't matter with `error` because it's returned everywhere, both functions and from packages by proxy. I'm not arguing that the tenet should be held true, to be clear. I'm saying that this tenet is misleading. If you can, return a concrete type. If several packages consume the same interface, then you it's not reasonable to define the interface at the consumer because you'd just have to copypaste it. | | |
| ▲ | 9rx 19 hours ago | parent [-] | | > I'm saying that this tenet is misleading. Doesn't that go without saying? There is no tenet that isn't misleading when presented to a general audience. Fair that if you come from a position where you understand the full context and nuance under which the tenet was built then you should be able to free yourself from being mislead, but, of course, this time is no exception. > If several packages consume the same interface, then you it's not reasonable to define the interface at the consumer because you'd just have to copypaste it. Where several packages find interface commonality, there is no doubt a set of "primary" functions that roll up shared functionality around that interface. The package of shared functions is understood to be the consumer under that scenario. Where several packages stumbled upon the same interface without any shared functionality, copy/pasting is warranted. In this case, while the interfaces may look the same, they do not carry the same intent and that needs to be communicated. Another oft-misunderstood tenet, do not repeat yourself, does not imply avoid repetitive code. | | |
| ▲ | the_gipsy 10 hours ago | parent [-] | | I don't quite see when a package is "understood to be the consumer" of... itself? We're talking about other packages importing an interface. I can give you a concrete example. I have a "storage" package, that exports multiple storage implementations/backends. Files, in-memory, S3, zip... Some other packages pick one of the implementations to instantiate, depending on the use case (this is NOT a case of mocking for testing or anything like that). Most other packages work with a "storage" interface, as they're only concerned with reading/writing to "storage". So the storage package, or in any case some package, has to export the interface. Otherwise, every consuming package would have to copypaste that interface, which is NOT warranted. | | |
| ▲ | 9rx 9 hours ago | parent [-] | | Actual code is always better, but based on the description it seems a bit strange that there would be a single package that exports multiple implementations. Presumably each distinct implementation should be its own package. None of these packages would export the interface. An additional package would export the interface and provide the common functionality around that interface. Those common functions would be considered the consumer. In fact, the standard library offers an example of exactly this: io/fs. | | |
| ▲ | the_gipsy 6 hours ago | parent [-] | | No, I cannot agree that this would be called the consumer. Yes, you have technically moved the interface type away from the implementation, but just for the sake of it, without any other upsides. The consumer is still the package that is using and importing this interface type, just from another package now. | | |
| ▲ | 9rx 2 hours ago | parent [-] | | That is the beauty of engineering: There is no universal truth, just different tradeoffs. Meaning that you don't need to agree, nor should you even seek agreement. You can and should forge your own path if different tradeoffs are warranted for your unique needs. But, this is the "idiomatic" approach. The upside is consistency for future readers. Code is for humans to read, after all. Most codebases follow this pattern, so it will be familiar when the next person encounters it. If you have a reason to do things differently, go for it. Nobody knows your problem better than you, so you cannot listen to others anyway. I am quite curious about what you see in the different tradeoffs you are accepting, though! What has you weighing them in favour? | | |
| ▲ | the_gipsy 34 minutes ago | parent [-] | | Sorry but I haven't really seen this pattern anywhere, care to give some examples? All libraries that I recall ever using always export a single package, including interfaces. I just took a look, and even io exports a bunch of structs along the interfaces they implement. And `error` is like a basic type of the runtime. | | |
| ▲ | 9rx 14 minutes ago | parent [-] | | > Sorry but I haven't really seen this pattern anywhere, care to give some examples? The standard library provides examples, including a "storage" example. > All libraries that I recall ever using always export a single package Right, and now there are questions around the language being spoken, so to speak. That's the power of idioms – it avoids the reader needing to ask new questions when encountering language that is new to them. But idioms are not the be all, end all. Sometimes they just don't fit. And if that's the case in your situation, no problem. Nobody knows your problems better than you. To listen to someone else tell you how to solve your own problems is foolish. That is why I earlier wished we had seen some real code. Perhaps then we would understand the nuance that has lead to you choosing these particular tradeoffs. We have no sense of what problems you are actually trying to solve. As a rule, though, an overarching interface package with consumptor functions and multiple implementation packages is preferable because then it avoids a lot of the questions developers are going to start asking. For example, with the alternative suggested, what if I want to add a new storage adapter that conforms to your interface? Do I need commit rights to your package or should I create my own package that satisfies your exported interface? If I create my own package, why is it the lone implementation in its own package while the others are all rolled up in one package? Is it that you don't want third-party implementations for other services not covered by your package? Why is that? On and on... If you stick to the idioms, those questions are already answered by convention. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ▲ | packetlost a day ago | parent | prev | next [-] |
| Rust and Go's lack of stack traces are basically equivalent in that you need to call an additional function to add the stack context to the error result. For go you use fmt.Errorf, in Rust you use .context from anyhow (bad practice in many contexts IMO) or .inspect_err + log. It's rather unfortunate that neither has an easy way of capturing a line number + file easily and appending it to the context. Go could easily do it, I think. Oh well. I agree that Go should really have an analogue to Rust's `?`, but you can't really do that in a sane way without sum types to represent your conditions. The very multiple-return style error propagation makes it impractical to do. IMO Go should add really basic tagged unions and rework the stdlib to use them, but such a change would probably necessitate a v2. |
| |
| ▲ | miki123211 a day ago | parent | next [-] | | > I agree that Go should really have an analogue to Rust's `?`, but you can't really do that in a sane way without sum types to represent your conditions. The very multiple-return style error propagation makes it impractical to do. There was a proposal for a `try`, which I still think should have been adopted. Under that proposal, `someComplicatedExpression(try(functionReturningError()))` would be converted to `foo, err := functionReturningError(); if err != nil{return zeroValue, zeroValue, err}; someComplicatedExpression(foo)` | | |
| ▲ | packetlost a day ago | parent [-] | | You would need some form of compile-time reflection/specialization to implement that properly (what if the second return value isn't an error? What if there's only 1 return value?). Further, you would lose the ability to add context to the error branch via fmt.Errorf, which seems rather critical to understandable error conditions. I'm not sure I would be satisfied with any implementation of try as the language is now, I assume the Go language team would probably feel the same. |
| |
| ▲ | dfawcus a day ago | parent | prev | next [-] | | > I agree that Go should really have an analogue to Rust's `?`, but you can't really do that in a sane way without sum types to represent your conditions. The very multiple-return style error propagation makes it impractical to do. There is always the Odin style 'or_return' operator, which is defined for a similar situation. https://odin-lang.org/docs/overview/#or_return-operator | |
| ▲ | pdimitar a day ago | parent | prev | next [-] | | RE: Golang v2, they clearly said they will not do it and will double down on backwards compatibility with exceptions powered by env vars and/or CLI switches. | | |
| ▲ | 9rx a day ago | parent | next [-] | | Technically, Go v2 signified the transition away from Google control to the project being directed by the community. That happened several years ago. Go v2 is already here and has been for a long time. The stdlib is also at v2 now (e.g. math/rand/v2). You must mean the language? They said that a language v2 (go2) is probably unnecessary – that any future additions could be added without breaking the existing language. I don't expect simple tagged unions would need to break anything. A v2 (or even v3, perhaps) stdlib would be necessary to take advantage, like the parent suggests, but that has never been ruled out and is already the status quo. | | |
| ▲ | packetlost a day ago | parent [-] | | This was what I was referring to. The stdlib is what would need to see backwards-compatibility-breaking changes, not the language itself. | | |
| ▲ | pdimitar 10 hours ago | parent [-] | | I am not sure how would that work? F.ex. how would you introduce tagged unions and make the language 100% backwards-compatible... but not the stdlib? I admit I have no idea. | | |
| ▲ | 9rx 8 hours ago | parent [-] | | The stdlib would remain 100% backwards compatible, but the implication was that he would want to see certain existing features of the stdlib amended with modified versions that leverage the new tagged unions. He imagined that modification would necessitate v2 stdlib packages to maintain sensibility. | | |
| ▲ | pdimitar 8 hours ago | parent [-] | | Oh. Silly me, I am ashamed for failing reading comprehension so badly. Thanks for clarifying, that makes sense. |
|
|
|
| |
| ▲ | packetlost a day ago | parent | prev [-] | | I'm fully aware of that. |
| |
| ▲ | the_gipsy a day ago | parent | prev [-] | | > Go should really have an analogue to Rust's `?`, but you can't really do that in a sane way without sum types It could be just some simple (hey, that's what go wants to be, right?) macro thing, that just does the everyday `if err!=nil{return ..., err}` for you without having to juggle (and think about) vars. b := g(f()?)?
// var b B
// {
// var a A
// var err error
// a, err = f()
// if err != nil {
// return *new(A), *new(B), err
// }
// var b B
// b, err = g(a)
// if err != nil {
// return *new(A), *new(B), err
// }
// // no accidental reuse of err
// }
I mean look how much utterly useless noise this is, and count all the opportunities for mistakes that wouldn't get caught by the compiler. |
|
|
| ▲ | dfawcus a day ago | parent | prev | next [-] |
| Alef actually has/had real tuples, as does Limbo. Looking at Alef, apart from using tuples to provide multiple return values in a C-like language, they don't seem to actually add much functionality over what Go has without them. One of the few extra things is the ability to pass a tuple over a channel, however passing a struct is more or less equivalent. I've not looked in much detail at what Limbo could do with its tuples. So maybe they don't really add that much, hence why they were not carried over when Go was created? Alef had enums, Limbo has something like the Go constant scheme and iota. Limbo also had tagged unions / varient-records, in a sort of Pascal like fashion - the "pick adt". |
|
| ▲ | 9rx a day ago | parent | prev | next [-] |
| > The weird "return tuple" , which obviously just exists for errors because there is not a single other place where you can use tuples in the language Go functions also accept a tuple on input. While theoretically you could pass an error, or a pointer to an error for assignment, it is a stretch to claim it is for errors. |
| |
| ▲ | the_gipsy a day ago | parent [-] | | Yes exactly, that rather useless feature just makes the whole thing even weirder. | | |
| ▲ | 9rx a day ago | parent | next [-] | | A function that passes tuples, or multiple arguments as they are more commonly referred to as, is not particularly weird, and has pretty much been the norm since the addition of functions to programming languages. But you are right that languages whose functions only accept tuples, but not return tuples, is a strange curiosity. In practice, you end up with developers packing multiple, unrelated items into a single variable to work around the limitation, which then questions why not do the same for input? | | |
| ▲ | the_gipsy 21 hours ago | parent [-] | | I meant weird/useless in the sense of that it's used virtually nowhere in practice. There is a nice func Must(value, err) { if err { panic(err) } else { return value } that you can use in tests to get around the inane regular error handling. But that's about it, to my knowledge. Sad that there isn't more to it. If literally everything returns error as last return value, there could well be some syntax sugar there. | | |
| ▲ | 9rx 21 hours ago | parent [-] | | I see it used often, even in languages that don't formally support multiple return arguments – with some hacked up array/object single value return to try and emulate what would be better represented as multiple return arguments. Like I mentioned in another comment, Go does seem especially prone to attracting developers familiar with other languages who insist on trying to continue to program in those other languages even after working in a Go codebase. That may create a condition whereby the developers you regularly come across don't have a good grasp of where to use multiple return arguments effectively, and thus end up avoiding them, even where they would be appropriate. With respect to the syntax sugar, there have been a number of proposals for exactly that. While the proposed syntax itself has been well received, nobody has come up with a good solution for the rest of the problem. Syntax is only about 10% of what is needed, of course. Rust, for example, has well defined traits and other features to go along with its '?' operator to fill in the remaining 90%. Presumably there is a good solution out there for Go too, but until someone proposes it... | | |
| ▲ | the_gipsy 12 hours ago | parent [-] | | The pragmatically "right" choice is to have some tuple type built-in. Because they are not only very good for function in/out, they can be used in many more places. Lists of tuples, tuples in structs, etc. If the language doesn't have tuples, then you have to "roll your own" and emulate them every single time, but it's not a functor so you can't do all the useful stuff. Go didn't do the pragmatically right choice, because it's only right from a type perspective. But go doesn't care about types at all, they're just a side effect. Go only needed to return multiple values, so that you don't have to pass in one (or more) "out" pointer as argument, and check an errnum. So go is right but only from its narrow perspective: it's better than C. | | |
| ▲ | 9rx 8 hours ago | parent [-] | | > The pragmatically "right" choice is to have some tuple type built-in. Perhaps, but in practice we end up with these ill-conceived languages that support tuples but end up not embracing them. Consider, for example, this Rust function: fn process_tuples(tuple1: (i32, i32), tuple2: (i32, i32), tuple3: (i32, i32))
If it made the "right" choice a function would only accept a single input value, which could be a tuple: fn process_tuples(tuples: ((i32, i32), (i32, i32), (i32, i32)))
But maybe that's not actually the "right" choice? What if there is a pragmatic reason for functions to have their own concept for tuples that is outside of a tuple type? I will admit, I would rather work with the former function even if it is theoretically unsound. It does a much better job of communicating intent, in my opinion.And once you accept the theoretical unsoundness of a function having its own tuple concept for input, it naturally follows that it needs to also extend that concept to returns. After all, the most basic function is an identity function. What would be super weird is not being able to write an identity function any time the number of inputs is greater than one. Go needed to allow multiple outputs because functions can accept multiple inputs. | | |
| ▲ | the_gipsy 6 hours ago | parent [-] | | > Go needed to allow multiple outputs because functions can accept multiple inputs. I don't think that's the right conclusion, unless you have any source or insight? It would explain why you can directly cast the multi-return-values into parameters when calling, but... that doesn't seem to fit go at all. What I would think is that "go needed multi-return to be able to return errors along values", and the mentioned feature is just some vestige. It's not like go ever implements anything for the sake of consistency or correctness. | | |
| ▲ | 9rx 2 hours ago | parent [-] | | > I don't think that's the right conclusion It is the "right" conclusion in general. Multiple input arguments without multiple output arguments is frustratingly awkward. I can understand the appeal of your suggestion of only accepting one input and returning one output, where the passed value might be a tuple, but I expect there is good, pragmatic reason why virtually no languages, even those with a proper tuple type, haven't gone down that road. Once a language accepts multiple arguments, though, it needs them in and out. Otherwise you can't even write an identity function. If you can't write an identity function, is it even a function? > "go needed multi-return to be able to return errors along values" If there was some specific reason for its addition, beyond the simple fact that it would be crazy not to, it was no doubt for map presence checks. Without that, it is impossible to know if a map contains a given key. Errors can be dealt with in other ways. It wouldn't strictly be needed for that purpose, although it turns out that it also works for that purpose, along with a whole lot of other purposes. Not to mention that we know that the error type was a late-stage addition, so there is strong evidence that errors weren't a priority consideration in the language design. | | |
| ▲ | the_gipsy 24 minutes ago | parent [-] | | With conclusion I meant you claiming that go returns multiple values because a function also takes multiple values. I did not refer to judging this as worse or better than having tuples as general types instead. > it was no doubt for map presence checks. Without that, it is impossible to know if a map contains a given key Are you just making stuff up on the go? A map access is not even a function call. And if you need a presence check in the early language development stage, it's much simpler to add support to check presence and then get the value, since it's not threadsafe anyway. |
|
|
|
|
|
|
| |
| ▲ | a day ago | parent | prev [-] | | [deleted] |
|
|
|
| ▲ | yegle a day ago | parent | prev | next [-] |
| https://github.com/pkg/errors provides stack traces support. This unfortunately did not get included in the Go1.13 release when error wrapping was introduced. |
|
| ▲ | devjab a day ago | parent | prev | next [-] |
| I prefer the structure of Rust errors as it’s fully typed, I don’t like that you can chain them though. It’s a cool feature but it leaves you with some of the same issues exception handling does when the freedom is used “wrong”. |
|
| ▲ | KingOfCoders a day ago | parent | prev [-] |
| Exceptions are sum types, they just have different syntactic sugar. |
| |
| ▲ | 9rx a day ago | parent | next [-] | | Checked exceptions may be implemented as a sum type. Traditional exceptions are more likely to be a single type that wraps up a context object alongside stack trace information. | |
| ▲ | a day ago | parent | prev | next [-] | | [deleted] | |
| ▲ | pema99 a day ago | parent | prev | next [-] | | Not really. Exceptions usually imply unwinding the stack, and the ability to catch at any point throughout the callstack. Result types are just 'dead' data. | | |
| ▲ | zozbot234 a day ago | parent [-] | | These are fully equivalent in outcome, though often not low-level implementation. You can use try...catch (called panic...recover in Go) to pack a normal and abnormal return case into the equivalent of a Result<> type. Or just pass an abnormal Result<> back to the caller to manually unwind a single "layer" of the call stack. | | |
| ▲ | biorach a day ago | parent [-] | | > These are fully equivalent in outcome They are so different in DX, ergonomics, implementation and traceability that I'm not sure this is true other than in the most abstract sense | | |
| ▲ | sshine a day ago | parent [-] | | There is some DX similarity between checked exceptions and Result types. Because the compiler will fail if you don't explicitly mention each possible exception. But checked exceptions are coming out of style: They're unchecked in C#, and frameworks like Spring Boot in Java catch all checked exceptions and rethrow them as Spring Boot flavored unchecked ones. For unchecked exceptions and Result types: The DX is very different in one critical way: With Results you constantly have to differentiate between error and ok states, before you proceed. With unchecked exceptions you generally assume you're always in an ok state. It's equivalent to wrapping your whole function body in 'try { ... } catch (Exception e)'. And you can get that with Result types in Rust by using '?' and not worry about doing something half-way. Ultimately: Are you a happy-path programmer? | | |
| ▲ | Arnavion a day ago | parent [-] | | >Because the compiler will fail if you don't explicitly mention each possible exception. But only the first time. Once you add `throws FooException` to the caller signature, the compiler won't complain about any future callees that also happen to throw FooException, even if you did care about handling their exceptions yourself. With callees that return Result you do get to make that decision for every callee. | | |
| ▲ | sshine an hour ago | parent [-] | | That's a fair distinction. I would say "adding Result to the caller signature" provides a similar cascade, but it's not entirely true: Every call must separately be unwrapped. So consistently wrapping your Result calls with the '?' operator is similar to a gigantic try-catch block or adding 'throws CheckedException' everywhere. So both the '?' operator and adding 'throws CheckedException' everywhere let you accidentally neglect proper error handling: You have a default that is syntactically almost invisible and frees you from thinking. The checked exceptions give you a little more freedom from thinking than the '?' operator. |
|
|
|
|
| |
| ▲ | veidelis a day ago | parent | prev [-] | | And different control flow, and different or sometimes non-existent types (Java's throws). |
|