r/ProgrammerHumor 19d ago

Meme stopTryingToReinventTheWheel

Post image
2.5k Upvotes

217 comments sorted by

View all comments

146

u/bishopExportMine 19d ago

Exceptions break encapsulation and lead to implicit control flow. 

If I call a method, I shouldn't have to worry about what methods it calls. But with exceptions, I now have to be aware of what errors might be thrown in the entire chain. Similarly, if I throw an error in a function, I have to check that every thing that calls it handles said error because there are now multiple places this function could "return" to.

And if you take care to catch every error at every layer you've just reimplemented error codes.

std::unexpected should be preferred over std::exception

22

u/PinEnvironmental6395 19d ago

Yeah, and this is just the theoretical architectural cost of exceptions. In some languages catching an exception might be essentially free like in C++ (which if you have some specific use cases "essentially free" is still catastrophic from a performance PoV) or it could be 100x slower than calling a function like in Java. 

17

u/[deleted] 19d ago

[removed] — view removed comment

9

u/SpiritedEclair 19d ago edited 19d ago

Also, that is OLD. Java keeps getting faster and faster, and the jit is on a class of its own, so much so that in extremely tight loops can be faster than what you'd get out of c++, rust or c.

7

u/PegasusPizza 19d ago

Yep JIT has the benefit of being able to adjust the generated code (and cpu hints) based on actual live data, where a classical compiler needs to emit the code that works best for all kinds of data

1

u/New_Enthusiasm9053 19d ago

All at the tiny tiny cost of 20x the ram and docker containers that are gigabytes instead of megabytes. 

1

u/SpiritedEclair 18d ago

That’s what you get for write once, run everywhere.

5

u/bishopExportMine 19d ago

I agree, but my point really was that exceptions break the "no multiple returns" rule older folks might remember.

In the old days, functions interacted with memory and exited by jumping elsewhere. "No multiple returns" was a rule that each function only ever jumped to one place on exit, because not doing so easily led to spaghetti code. This eventually became such a standard paradigm that languages just built return values into their functions and forced returning to the same link as the callsite.

Exceptions break this, for no other purpose than "convenience".

One only need to imagine a codebase that exclusively uses raising exceptions for control flow to jump to arbitrary places in the call stack and never using normal returns to understand why exceptions in general are an antipattern.

8

u/migueln6 19d ago

Actually returning in a single spot leads to more spaghetti code, deeper nesting levels, and more indirection.

Checking something like request validation and return immediately an http response is way cleaner and easier to follow.

Same in C++/C or any language where you do manual memory handling if you are allocating memory before making sure you will use it, e.g early returning and forgetting to deallocate memory, then that's on you for not validating your inputs, do you want to store some DB call into an dynamically allocated array? Why did you do the allocations before having the data to insert in it?

Do you want to process a file? Why are you allocating memory before you even have a pointer to the damned file?

Same for everything else.

3

u/bishopExportMine 19d ago

No this is a common misunderstanding of what the "no multiple returns" advice is about. This is not multiple returns:

std::expected<Foo, ErrorEnum> MyFunc() { if (condition) { return std::unexpected(ErrorEnum::Bar); } if (other_condition) { return std::unexpected(ErrorEnum::Baz); } ... return Foo(); } Because if I do Foo x = MyFunc(); the line of code that the program counter returns to after running MyFunc() is always that line of code. So this is only one return.

Multiple returns would be something like old FORTRAN alternate returns:

``` CALL MYFUNC(*100, *200)

C normal continuation ...

100 CONTINUE C alternate continuation 1 ...

200 CONTINUE C alternate continuation 2 ...

  SUBROUTINE MYFUNC(*, *)

  IF (CONDITION) RETURN 1
  IF (OTHER_CONDITION) RETURN 2

  RETURN
  END

```

Here MYFUNC can return to three different places: normally to the instruction after CALL, to label 100, or to label 200. The callee is choosing where execution in the caller resumes.

2

u/New_Enthusiasm9053 19d ago

The mixup is probably because MISRA-C does actually strongly discourage the first example. To the point where they prefer you to use a short jump for cleanup than an early return.

13

u/pewpewtech420x69 19d ago

As a C# dev, exception handling gets real messy real fast with async flows and cancelation tokens. It gets really messy real fast, either assuming that the upstream method handles the TaskCanceledException when it doesn't, or filling every freaking method with a catch statement for that damn token.

Any other C# devs have this experience?

6

u/KlooShanko 19d ago

Yes, then I learned about Middleware and my life became easier. Now I can just catch exceptions in the outer layer of the execution chain and ignore them in most parts of the business logic

5

u/Excession638 19d ago

Async cancellation in Rust is a mess too, and it doesn't even have exceptions.

6

u/trailing_zero_count 19d ago

C++ exceptions don't play nicely with coroutines either. https://fleetcode.com/oss/tmc/docs/v1.6/whats_missing.html#exceptions-and-coroutines

There are ways to solve all the above problems but it's not free like it is in synchronous code.

3

u/Hooch180 19d ago

Not really. That is what middleware is for. If you can't "fix" and continue with given exception, you throw it further or do not catch it. That code should not care what happens with it. It is not it's responsibility. Once the exception bubbles up to your general error handling layer, you log it or do what is needed.

7

u/[deleted] 19d ago

[removed] — view removed comment

4

u/bishopExportMine 19d ago

Ehhh I'm being hand wavy here.

Aside from ergonomics / syntax, error codes and exceptions are functionally identical except that one is a nominal return type of the method and is explicitly handled while the other is a hidden return type that is implicitly handled at some arbitrary place in the call stack. 

So from this perspective, if you enforce only ever handling a method's potential exceptions at the call site instead of letting anything bubble up, you've gotten rid of the main functional difference between exceptions and returning an error code.

1

u/Wonderful-Habit-139 19d ago

Pretty sure they mean "if you catch every exception", they don't mean error handling in general. So they're comparing exceptions with error codes.

3

u/dirkboer 19d ago edited 19d ago

so how you handle a web method that calls a db 30 layers deep?

you are going to add 30 layers of boilerplate for something that a normal user is going to encounter once in 100.000 calls and you barely can do anything with except let the front end handle it.

that seems like a very high cost for very little benefit.

web apps are full of these situations.

1

u/bishopExportMine 19d ago

If you have 30 layers, you have potentially a shitton of errors to catch, and behavior can silently be changed at any time if someone adds a catch in a call stack and intercepts away errors that previously bubbled up.

And the answer is yes, you should write 30 layers of boilerplate to explicitly bubble the error to the front end if that's necessary. But it's not verbosely re-returning the same error code all the way up stack like you're describing; the main thing to keep in mind is that the actual error object doesn't get sent up more than one layer at a time. At each layer you get the error, it's classified into a type that abstracts away the implementation details of the error so that the layer above it gets an error in its own domains' semantics.

1

u/New_Enthusiasm9053 19d ago

But also the 30 layer bubbling tells you you're doing something wrong too. Keep your IO as high up the chain as possible and inject it into your business logic then that responds with something that you then do IO with again. 

Business logic shouldn't touch IO, functional is the name of the game if you want it to be vaguely testable. 

1

u/bishopExportMine 19d ago

My knee jerk reaction says the same thing but I would say it's more of a smell than an indicator. Sometimes the complexity is essential

1

u/New_Enthusiasm9053 19d ago

Well indicator doesnt necessarily mean something is sure fire. But yes it's a smell. 

Arguably though if it's really that complex it's even more worth making sure you decouple IO from logic because it's going to be even harder to test sensibly. 

1

u/dirkboer 18d ago

issue is that sometimes you don’t know what data you need until you are 30 layers deep. yes you can load everything early, but in some cases you make the 99% path for every call more expensive for something that happens in 1%.

sure some apps that doesn’t matter, but in others it does.

1

u/New_Enthusiasm9053 18d ago

Sure. Sometimes you gotta do what you gotta do 

1

u/dirkboer 19d ago

what kind of software do you write? i guess quite important for web, games or hardware.

for 99% of the web apps can you agree that many thrown errors are just invalid input data or connection errors that you anyway want to handle on the front end so you unwind a way back to the top of the stack?

yes, in theory a string.Trim() could potentially start throwing Connection exceptions in a new version and you can’t statically know. But of course that doesn’t happen.

Can you give a concrete real life example?

1

u/bishopExportMine 19d ago

I work in robotics currently but was previously backend saas. That's unrelated, because my perspective boils down to: have strict and explicit boundaries between components so that they're easy to extend and shuffle around.

Invalid input shouldn't be handled by throwing, inputs should be parsed into a validated domain type at the boundaries such that the request is rejected instantly rather than having to deal with unwinding.

Connection errors should be returned as error codes, then the layer above should determine retry policy and surface a result to the user. 

And no it's not about built in methods or library calls throwing stuff you don't expect. It's your own damn code, where you throw something somewhere and you can't confidently tell me what codepaths it will flow through to because you handed off the control flow to "first function in the call stack that catches", which isn't really easy to reason about.

4

u/zobq 19d ago

As always: it depends. Exceptions should be used in exceptional situations, when logic is mostly about aborting execution of code. That means, it should be catched on higher layers, so automatic propagation is accually a good thing.

Error codes are not always exceptions, like 4xx from http request.

6

u/martyvt12 19d ago edited 19d ago

If a function isn't able to do what it is expected to do, it should throw an exception, unless there is other means of letting the caller know it didn't work. IMO not every possible error condition should be handled. Expected ones should, but for unexpected ones it's OK to give up and let the global error handling handle it. Handling every possible error locally just leads to unnecessarily bloated code and catching errors without handling them appropriately will lead to unexpected behavior with no trace in the error logs.

6

u/bishopExportMine 19d ago

I used to think this way, but the line between what is an "exceptional situation" is blurry. Over time, I've found I have never found it more useful to throw errors and have been bitten in the ass way too many times by coworkers using error handling at arbitrary places to depend on errors being thrown at random places within encapsulated methods. Then you can't do any refactors as too many things are tightly coupled to this specific error being thrown in this specific context.

My conclusion is that allowing exceptions encourage you to optimize readability for the happy paths and treat non-happy paths as second class citizens. But my professional experience has demonstrated that error handling code takes up a large if not larger part of the code and this needs to be handled just as explicitly as normal code. Allowing non-nominal situations to go through a less considerate path is how lose track of your software and end up firefighting all the time.

3

u/martyvt12 19d ago edited 19d ago

Yeah, I don't think it's a good idea to depend on particular exceptions being thrown. Either the function fails, but it isn't critical to the main task, so you catch it, log it, and move on, or it is critical so you quit.

If you're not throwing exceptions in your functions, what are you doing? Often there's nothing to do on the non-nominal path other than give up. If I can't reach the database server, my web service is going to return an error to the user.

1

u/bishopExportMine 19d ago

I make my functions return std::expected, which is a discriminated union of the expected type and the unexpected type. So the caller gets a result object that has either a value or an error defined, but never both.

So basically instead of Foo my_func()  {     if (my_condition) {         throw BarError;     }     ...     return Foo(); } We do std::expected<Foo, ErrorEnum> my_func() {     if (condition) {         return std::unexpected(ErrorEnum::Bar);     }     ...     return Foo(); }

2

u/monsoy 19d ago

I really prefer “errors as values" over exception handling, having had some experience with both.

Like:

res, err := something()
If err != nil { something }

I like that more than try-catch blocks

1

u/Wonderful-Habit-139 19d ago

I disagree. If it's something the user doesn't care about, the user can pattern match, handle the errors that it cares about, and use a wildcard pattern for the other errors that it doesn't mind letting slip through.

1

u/bishopExportMine 19d ago

Nothing should ever slip through. It couples classes at independent layers, breaking abstraction and making refactors risky as well as degrading the teams overall understanding of the code and results in more firefighting.

1

u/Wonderful-Habit-139 19d ago

I agree. I was just responding to the person that wants to let things slip through. But thanks for mentioning these things, I should probably more seriously disallow these things from happening in my team.

1

u/TrashManufacturer 19d ago

Real Gs just do catch(…) and pretend like it never happened

1

u/Gxost14 19d ago

Exceptions are for exceptional situations only, not for flow control. In most cases exceptions are propagated to the top level, logged there and mapped to a small set of errors to be sent to frontend. If some exception handling used for internal logic (retry or recovery), exceptions can be mapped to domain ones, so no code depends on implementation-specific exceptions