JavaScript/TypeScript has no checked exceptions and this bothers me.
As far as I know, Java
introduced checked
exceptions whereby a method signature advertises that a method or
constructor may throw an exception of a specific type. For example
the write
method of an OutputStream
is declared as:
public void write(int b) throws IOException { ... }
When another method calls it, it must do one of two things:
catch(IOException e) {...}
clause to catch
and handle it, or
try { out.write(0xa); } catch (IOException e) { // do something reasonable }
throws IOException
.
void writeStuff(...) throws IOException { out.write(0xa); }
In the first case it is blatantly obvious that calling write()
either works or not and this other case is handled in some
way by the catch
clause.
The second case can be somewhat less obvious if the body
of writeStuff
gets longer as it is not clear from which call the
declared IOexception
originates.
Java also provides unchecked exceptions where neither a catch
clause nor a throws
declaration is required. Nevertheless
a catch
clause may be written somewhere up the call stack after
carefully reading the documentation and/or inspecting the signatures of called
methods down the stack in all their breadth to figure out which unchecked
exception may pop out.
The unchecked exceptions are what JavaScript solely provides. Any function may throw stuff any time and upwards the call hierarchy you need to do as described above: do some hard work on docs and with your IDEs to figure out whether to better catch something or not.
A third approach is for a method or function to return either the normal result or the "exception". In TypeScript this could be written as:
function f(in: SomeIputSource): string | IOException { ... }
In contrast to throwing exceptions, this requires even more explicit action
at the caller. It is similar to a checked exception, yet without the automatic
re-throw we have if the caller declares throws IOException
, as is
possible in Java. This is
what Rust
opted for.
So basically there are three ways to deal with what is customarily called "errors" or "exceptions":
There is an ongoing discussion about whether or not to use checked or unchecked exceptions, just do an Internet search. From the wording above you may have guessed that I am not fan of unchecked exceptions. The arguments are all made.
Or not? I think Rust comes close, but let me explain.
The Result<R,E>
construct used
in Rust
is a great step the the right direction as it puts the "normal" and the "other"
result nearly on par. To improve the base for discussion and guide the thinking
I suggest:
Do not call the "other" result type "error" or "exception".
I cannot come up with a better name currently than "other". Nearly 50 years of training nudge me into calling it a lot of things, but all with a negative connotation. But the other result should not be seen as bad, exceptional, error, unsuccesful, failed or whatever. Opening a file with a name for which there is no file is normal business. Why treat it differently than the lookup of a key in a map. It may mapped to a value or not. Both is possible, neither is good or bad, they are on par and should be treated respectively.
The same holds for requesting data from an HTTP server. The resource was changed, the server is down, the network is down, the data format is different from what the software at this point expects. There are gazillion reasons why we might not get the nicely formatted JSON describing the customer address, yet all those gazillion reasons are "exceptional"? Seriously? Maybe getting the nicely formatted JSON should be called the exception. :-)
As another, slightly more tricky example consider the compilation of a regular expression into an internal structure. Two differing use cases can be considered (Java example):
Pattern num = Pattern.compile("[0-9]+");
Pattern grep =
Pattern.compile(System.getProperty("pattern"));
The first example uses a pattern literal string provided by the
developer. Being forced to handle an alternative result, like a description of a
potential syntax error would be extremely cumbersome. Ideally the compiler (as
many IDEs do today) would verify the pattern already, but the second best
solution in this case, as an exception from my avoidance of unchecked
exceptions, would be an unchecked exception or, as in Rust,
a panic!
.
In the second example, the regular expression is obtained from an external
source, like provided by a user, so it is normal business that the string
provided is not a regular expression pattern. Rather than just returning,
say, null
as in the case of a key lookup in a map, it is quite
natural to return a description of why the string is not a pattern,
customarily called a syntax error.
Java opted for the former, so when trying to compile non-literal strings, the developer is not reminded by the compiler: "hey, the string may not be a pattern". The developer must waste precious brain capacity to remember that this may be the case and add the respective handling voluntarily.
But I think the two examples above show that both use cases are relevant and in such a case I see only one way out: there should actually be two pattern compile function. One with "other" result and one doing the "unchecked exception" or "panic" thing.
I think the debate about checked or unchecked exceptions would be over quickly if we stop calling checked exceptions "exceptions" at all. As soon as we accept that those are just a second type of result, on par with what we call the main result so far, it should become much more easy to recognize whether we really are in an "unchecked exception"/"panic" use case or not.
Personally I'll want to start using the Rust approach in TypeScript too, in particular as union types make this so easy. The explicit handling on each level in the call hierarchy may look slightly cumbersome. Yet: the more explict your code, the less you have to remember or reconstruct when you come back to it in a year's time.
And for quality software, its function must be so blatantly obvious that everybody thinks: wow, this is trivial code, even a monkey could've programmed this.
If your're a junior developer and are proud of "wow, I wrote this complex code and I understand it in every detail", make sure to change employer to avoid looking at this code in some time, because you will hate yourself.