
The Throw Keyword Was a Mistake - _1
https://hackernoon.com/the-throw-keyword-was-a-mistake-l9e532di
======
kova12
I disagree. Throw keyword is the greatest invention, and makes programming so
much better. Error codes are the evil. Exceptions allow to separate business
logic from error handing in a most effective manner. Author likely have not
ever worked on large codebases where the error handling was messed up beyond
any repair due to having to pass error codes every friggin where

~~~
jwilliams
The opposite to exceptions isn't (necessarily) error codes. Golang errors are
a recent example.

~~~
tomp
How does that work? I haven’t seen the recent changes so I can’t say for sure,
but AFAIK Goland and Rust error handling mostly amounts to line noise trying
to recreate the control flow of exceptions (i.e. bubbling any errors up the
call stack) (although Rust tries to make it more palatable using macros).

~~~
wizzwizz4
Then don't try to recreate the control flow of exceptions. Handle things ASAP;
only propagate an `Err` if the function returning a `Result<_>` _logically_
has an error condition. Don't be afraid to panic, unless user data is on the
line, in which case register an unwind handler and tiny backup facility if you
don't trust your programming ability (hint: nobody should trust their
programming ability _that much_ unless they've rigorously (mathematically)
proven their code's soundness).

It's a different way of programming, but a more explicit one, and I don't
think implicit Bad Thing™ should be easy in a language.

(By the way, Rust has a single-character operator to emulate exception pass-
through.)

~~~
tomp
> Don't be afraid to panic, unless user data is on the line, in which case
> register an unwind handler

Sounds like exceptions.

------
al2o3cr

        only the language runtime and operating system
        would be allowed to raise exceptions
    

So you still need all the exception machinery, but "normal" code can't use it?
Seems like a terrible situation.

Also, who's in charge of deciding what's "language runtime" and what isn't?

~~~
__s
This is how it's done in Rust. panic macro unwinds stack for tracebacks,
there's std::panic::catch_unwind, but panic can be compiled to be aborts to do
away with stack unwinding, error handling is meant to be done with Result<T,
E> type, whereas panic is meant for unrecoverable errors

~~~
riwsky
This is not an apples-to-apples comparison. In Rust, app code _can_ panic,
even though it’s not idiomatic. That does not meet the standard prescribed by
the quote from the article.

------
mrkeen
> The Year Everything Crashed

Which year?

> If you were using computers in the years that followed you remember the time
> well even if you didn’t know the reason; a lot of software that had been
> stable and solid before started crashing and misbehaving.

I think I missed this.

~~~
arpa
Well lp0 would catch fire, but it wasn't as bad as using throw!

------
bluestreak
Throw in Java is very inexpensive. Expensive part is creating new instance of
exception, which figures out stack trace and creates bunch of immutable
strings.

There is a technique of throwing existing static instance of exception, which
generates no garbage and two orders of magnitude faster than traditional java
approach:

[https://github.com/questdb/questdb/blob/7560895ee400c82b0e5e...](https://github.com/questdb/questdb/blob/7560895ee400c82b0e5e39440eaf6c55bbbb7d7d/core/src/main/java/io/questdb/cutlass/text/types/InputFormatConfiguration.java#L207)

The only downside is that stack trace is effectively defunct.

------
dkulchenko
I love the way Elixir/Erlang handles this, personally.

If the error is expected/should be handled, just add the error to your pattern
match and handle it right there (or bubble it up, if appropriate).

Truly exceptional situations where something "should never happen" won't be
pattern matched against, BEAM will crash the process, and your supervisor will
get you back to a good state.

Feels so much cleaner than either exceptions everywhere or easy-to-ignore
error values.

------
fxtentacle
I strongly believe that the Go approach is the correct way to go. Every
function that can fail should return an error code and then the programmer
invoking said function is expected to handle the error code. It also used to
be that way back in the old C / C++ days before SEH made it messy like Java.

And the beauty of using statically typed languages with error code return
values is that one can use automated tools to find all the places where error
codes are being ignored. So then random error conditions sprinkled throughout
business logic go back from "this is how Java is done" to "this is a code
smell".

~~~
wruza
>Every function that can fail should return an error code

Every function that can fail, that can take invalid arguments, or can be
called at invalid times. Which widens the set to basically every function. Old
APIs had a convention to always return a status directly and results in out-
params, except for pure mathematical operations (in which errors were mostly
ignored, e.g. in a computer, a+b may fail as much as fopen(), but who cares).

Observing this fact, one can free themselves from:

    
    
      if ((status=operation(args, &vague_error_descriptor))
        return status;
    

and handle the specific error event where it seems reasonable in the way that
seems reasonable.

Unwinding call stack by hand doesn’t automagically handle errors. At main() or
at the other high-level call site you still have no idea what to do with an
unexpected error except logging/presenting it and exiting (or continuing in
hope invariants were not broken).

>one can use automated tools to find all the places where error codes are
being ignored

The same is true for exceptions - every milestone like db.open(), file.save()
and process.spawn() should be try-catched. And in both cases you don’t know
which errors “codes” are still unhandled.

~~~
loopz
You are describing panic() which may happen due to programmer error, but not
from a+b or any arbitrary event. It follows a short spec and stdlib, and may
even be recoverable. Your if-example is old style and not allowed in golang.
Your milestones escalates any small deviation into potential crash-handling
territory (good example).

------
DictumMortuum
I worked at a telecom company. They had a program written in java for
generating the initial customer configuration.

Among other stuff it used exceptions as control flow and it made me make
extraordinarily high estimations for seemingly simple changes.

Anyway, I don't think I agree with the article - it's again the matter of the
knife and how you use it.

~~~
notacoward
> it's again the matter of the knife and how you use it.

The author makes this point repeatedly: exceptions would be great if all
programmers were above average, but (of course) they're not. _In practice_ the
knife is juggled and thrown (heh) and generally mishandled so much that the
floor is covered in blood. This gets us into pg's whole "blub" design-for-
geniuses vs. design-for-idiots discussion, with is basically dueling strawmen,
so I'll bow out there.

------
scarejunba
We've moved past it now with having errors just be part of the type and
signature so you can use language tools to deal with them like any other type
instead of having special syntax. I like that. It allows you to express
certain concepts more easily. For instance, "in case of failure, use blank"
looks nicer in Rust than in Java, and so you're more likely to use it. Java is
also sort of moving this way because checked-exception methods don't play well
with Streams syntactically.

------
kazinator
> _Structured Exception Handling (SEH) was born._

This, including the abbreviation, is the name of a feature of the Windows API.

The author is confused between exception handling in programming languages,
and exception handling in the MS Windows operating system.

[https://docs.microsoft.com/en-
us/windows/win32/debug/about-s...](https://docs.microsoft.com/en-
us/windows/win32/debug/about-structured-exception-handling)

[https://en.wikipedia.org/wiki/Microsoft-
specific_exception_h...](https://en.wikipedia.org/wiki/Microsoft-
specific_exception_handling_mechanisms#SEH)

Note that in Microsoft C++, this uses special ___try_ , ___except_ and
___finally_ keywords, which use underscores because they are extensions,
separate from the ISO C++ _try_ and _catch_.

SEH can catch events like access violations. It's more like Common Lisp
exception handling in that you can catch errors without any stack unwinding
taking place, so that fixing up a fault and re-starting an instruction is
possible (like catching a SIGSEGV or SIGBUS on POSIX).

------
bvrmn
Error propagation is must have for any project. You always need to handle some
errors in parent context because local knowledge can be not enough. And you
can choose between manual passing of error codes (c, go, mundane and boiler-
platy), algebraic errors (rust, zig, optional sugar can make it convenient and
look like SEH) and SEH. Yes it is additional condition flow, but you have to
introduce it in some way.

------
fuzzy2
Didn’t Java require all exceptions be declared in the method signature?
(Haven’t used it in years.)

I think that’s the best solution. You declare what error conditions could
arise, but not using magic numbers. The caller can then decide whether to
handle them or pass them to its caller etc.

I agree that unexpected exceptions (like it’s possible in C# and possibly
others, you can just throw whatever whenever) are far from optimal.

~~~
riwsky
No. Only a certain subset, known as “checked exceptions” (as opposed to
“runtime” exceptions), need to be in the signature.

In what may come as a surprise, checked exceptions are actually frowned upon
in modern java (e.g. new stdlib apis tend to avoid them). They do not compose
very well with the rest of the type system (e.g. you cannot write a ”map”
function that is polymorphic over checked exception types)—or at the very
least, do not compose as well as some other patterns, like Optionals

~~~
fuzzy2
Hm okay. If it’s not “compatible” with streams, higher-order functions and
whatnot I can see why it’s not popular. A pity, really.

------
ensiferum
The problem comes when programmers muddle and get confused about different
possible "failure/error" conditions in a program.

A) Bugs, i.e. errors made by the programmers of the software B) Expected
logical conditions that only appear as "errors" to the _users_ C) Unexpected
failures such as resource allocation failures

Let's have a look at these one by one.

Case A, i.e. bugs written by programmer, i.e. the program is about to violate
it's own constraints/logic/pre/postconditions/invariants. Examples are many,
but for example off by one, accessing nullptr/null object, array out of bounds
etc. Really the best way to deal with these is to abort/panic/dump core with a
loud bang and produce a core dump and a stack trace to simplify debugging.

Case B, i.e. conditions that the application should expect to encounter and be
prepared to deal with. For example your TCP socket connection times out, DNS
query fails, HTTP query response returns 404. In any such case when you need
to write some logic such as "retry to open the sockect connection" or "tell
the user that the resource was not foudn" or try to resolve DNS query again
later the best method is to use some kind of error enum/number/code kind of
system. And to recap the "errors" in this category are not errors for the
software but only errors for the _user_. I.e. for the software these are just
logical conditions.

Finally Case C), errors that are neither A or B. Typically these are just some
very unexpected conditions such as resource allocation failures. Your program
failed to allocate a socket or pipe or a mutex or whatever. Should not happen
but _might_ happen once in a blue moon under heavy system load or unexpected
conditions. These are the things you don't want to conflate with your logical
"error condition" path from case B because't that won't scale and propagation
of errors up the stack becomes very tedious. These are best suited for
exceptions.

Edit: Note that this isn't a static state but what is considered a bug or a
logical error _can_ change across modules depending on the software project
organization. A module written by another team migth consider illegal
arguments as "Logical error conditionds" and return error codes, whileas a
similar module written and used within a single team might just consider this
a bug and dump core.

~~~
notacoward
Agreed. Exceptions should be, well, exceptional. Not normal parts of control
flow, or commonly occurring kinds of errors. Assertion failures, or other
things that would otherwise send a signal and terminate the process - seg
fault, divide by zero, etc. _Maybe_ something like a socket being closed
unexpectedly. That's kind of the boundary where it's worth debating. Anything
more common/expected than that should be represented as an error code and not
completely redirect the program's control flow.

------
ch_123
> Dealing with them in a more elegant way than inelegantly crashing goes all
> the way back to 1962 in a near-forgotten language called LISP.

Even as someone who isn't a Lisp programmer, I find the idea that Lisp is a
"near forgotten language" to be a bit of a stretch.

------
javajosh
I agree that `throw` is easy to misuse; but I think this is a symptom of
something else, namely in architectures where input even handler code is
spread all around or, worse yet, dynamic. This is really common in many
languages, and perhaps my greatest pet peeve.

However, throw is a wonderful facility if your architecture has one defined
entry point for inputs of all kinds. It means your outermost loop can have a
try block, and literally everything else can feel free to throw in the safety
and comfort of knowing the entire program won't fall down.

~~~
loopz
goes on to explain misuse.. jk sort of.

What when allocation fails or disk full etc?

~~~
bvrmn
> What when allocation fails or disk full etc?

It is a common misconception that such errors can be easily handled. Most of
software is too complex to be able to do something reliable with hard OS
issues. Stack unwiding with exceptions is the most quick way to disaster with
ENOMEM as root cause.

------
playpause
Can someone point me to a code example (preferably TypeScript or JS) to
clarify what the author proposes as an alternative to ‘throw’? Maybe it’s
Stockholm Syndrome, but I’m struggling to picture how returning an error code
(and checking it with an if-statement after every function call) would make
code easier for a human to analyse. Seems like it would just make it a lot
noisier and less expressive. But I’m intrigued.

------
lilyball
The keyword, and exceptions, are orthogonal concepts that this post is
conflating. The real issue this post has is with exceptions (and in particular
user-defined exceptions, though I'm not really sure why this matters because
you can always (ab)use system exceptions if you have a way to reliably trigger
them). You can still use the `throw` keyword with other error-handling models,
such as with Swift's Errors.

------
foreigner
Could some sort of hybrid approach be an improvement? What if we required
every function to explicitly declare the types of exceptions it might throw?
The compiler could enforce it and make it transitive to higher level
functions.

~~~
ben509
That was the basic premise of checked exceptions in Java.

It's not really related to the type system or anything. The root of the
problem is Java compiles each class as an independent module, so any method
has to deal with all its exceptions, even if only naming them, or it can't
compile. Thus checked exceptions meant you either got huge signatures, or you
wound up wrapping them all into other exceptions, which then made them much
harder to inspect.

And unchecked exceptions made the problem worse. Java uses single-inheritance
and put unchecked exceptions on their own branch. This meant you wound up
with, for example, IOException and UncheckedIOException with no close common
ancestor, so you had to wrap one in the other. So now, in addition to
complicated signatures, you are doing more wrapping.

I think what might work would be to ditch the checked / unchecked idea,
mostly. (You still want to treat errors like division by zero or invalid array
indexing as unchecked.)

What you distinguish instead is whether methods are "safe" or "unsafe". An
unsafe method can still catch some exceptions, it just doesn't promise to
catch all of them.

The safe method must catch everything. For example, your main method and
Thread.run would have to be safe.

------
lwhi
When 'pair programming' was chucked in as an example of one of the worst
things that happened to programming I lost interest.

Back up your assertions with facts. If you don't have facts use good truthful
examples.

Too much opinion and conjecture isn't useful.

~~~
notacoward
Rejecting the entire main argument because of a single aside - a matter of
style more than substance - is effectively argumentum ad hominem. Yes, the
author comes across as a grumpy old "get off my lawn" type (like me in fact).
No, that doesn't make any of their arguments or anecdotes about throw invalid.
Try addressing the issue, not the person.

~~~
lwhi
While you may be able to sympathise with the author's style of writing, I
think my point still stands.

~~~
notacoward
Your point seems to be that it's OK to reject an entire argument because of
one irrelevant aside. I guess that "stands" as a personal belief, but it does
not stand as an argument or refutation of one.

------
classified
Publishing on hackernoon is a mistake. The site sucks and also disables the
Reader View that would make it tolerable.

~~~
bvrmn
No, article corresponds to unicorn-rainbow-coin-rebel style of most hackernoon
content very well.

------
altitudinous
throw is being used as a replacement for goto.

~~~
jnwatson
All control flow are jumps underneath it all. The only important question
about a particular control flow syntax element is: does it allow a human to
reason about a particular code scenario more easily.

Most of the time, I think exceptions help more than hurt. Like many things in
language design, it is a trade off, in this case of concision vs simplicity.

~~~
fxtentacle
I've always thought of Java as being extra verbose, in part because there's
try catch blocks everywhere.

~~~
tsimionescu
Wherever there is a try/catch block in Java, you would have at least a few if
err return/goto cleanup in C/Go/other error-code languages.

This is especially true when you have a function which performs multiple
operations that return the same kind of error (e.g. multiple HTTP calls, or
multiple file-based operations). In general, in Java you need a single
try/catch block for all of these (which may well be in another function), but
you need an if err return for each one in C.

Rust has a more interesting approach, where the sue of Macros gets rid of the
verbose if-based pattern, but the code also avoids non-local return. I haven't
written Rust yet, but it seems like a very interesting approach.

