14

Some languages claim to have "no runtime exceptions" as a clear advantage over other languages that has them.

I am confused on the matter.

Runtime exception is just a tool, as far as I know, and when used well:

  • you can communicate "dirty" states (throwing at unexpected data)
  • adding stack you can point to the chain of error
  • you can distinguish between clutter (e.g. returning an empty value on invalid input) and unsafe usage that needs attention of a developer (e.g. throwing exception on invalid input)
  • you can add detail to your error with the exception message providing further helpful details helping debugging efforts (theoretically)

On the other hand I find really hard to debug a software that "swallows" exceptions. E.g.

try { 
  myFailingCode(); 
} catch {
  // no logs, no crashes, just a dirty state
}

So the question is: what is the strong, theoretical advantage of having "no runtime exceptions"?


Example

https://guide.elm-lang.org/

No runtime errors in practice. No null. No undefined is not a function.

  • I think it would help to to provide an example or two of the languages you are referring to and/or links to such claims. This could be interpreted in a number of ways. – JimmyJames Dec 1 '16 at 16:50
  • The latest example I have found was elm compared to C, C++, C#, Java, ECMAScript, etc. I have updated my question @JimmyJames – atoth Dec 1 '16 at 16:52
  • 2
    This is the first I heard of it. My first reaction is to call BS. Note the weasel words: in practice – JimmyJames Dec 1 '16 at 16:55
  • @atoth I'm going to edit your question title to make it clearer, because there are multiple unrelated questions that look similar to it (like "RuntimeException" vs "Exception" in Java). If you dislike the new title, feel free to edit it again. – Andres F. Dec 1 '16 at 17:08
  • OK, I wanted it to be general enough, but I can agree on that if that helps, thanks for your contribution @AndresF.! – atoth Dec 1 '16 at 17:44
27

Exceptions have extremely limiting semantics. They must be handled exactly where they are thrown, or in the direct call stack upwards, and there is no indication to the programmer at compile time if you forget to do so.

Contrast this with Elm where errors are encoded as Results or Maybes, which are both values. That means you get a compiler error if you don't handle the error. You can store them in a variable or even a collection to defer their handling to a convenient time. You can create a function to handle the errors in an application-specific manner instead of repeating very similar try-catch blocks all over the place. You can chain them into a computation that succeeds only if all its parts succeeds, and they don't have to be crammed into one try block. You are not limited by the built-in syntax.

This is nothing like "swallowing exceptions." It's making error conditions explicit in the type system and providing much more flexible alternative semantics to handle them.

Consider the following example. You can paste this into http://elm-lang.org/try if you would like to see it in action.

import Html exposing (Html, Attribute, beginnerProgram, text, div, input)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import String

main =
  beginnerProgram { model = "", view = view, update = update }

-- UPDATE

type Msg = NewContent String

update (NewContent content) oldContent =
  content

getDefault = Result.withDefault "Please enter an integer" 

double = Result.map (\x -> x*2)

calculate = String.toInt >> double >> Result.map toString >> getDefault

-- VIEW

view content =
  div []
    [ input [ placeholder "Number to double", onInput NewContent, myStyle ] []
    , div [ myStyle ] [ text (calculate content) ]
    ]

myStyle =
  style
    [ ("width", "100%")
    , ("height", "40px")
    , ("padding", "10px 0")
    , ("font-size", "2em")
    , ("text-align", "center")
    ]

Note the String.toInt in the calculate function has the possibility of failing. In Java, this has the potential to throw a runtime exception. As it reads user input, it has a fairly good chance of it. Elm instead forces me to deal with it by returning a Result, but notice I don't have to deal with it right away. I can double the input and convert it to a string, then check for bad input in the getDefault function. This place is much better suited for the check than either the point where the error occurred or upwards in the call stack.

The way the compiler forces our hand is also much finer-grained than Java's checked exceptions. You need to use a very specific function like Result.withDefault to extract the value you want. While technically you could abuse that sort of mechanism, there isn't much point. Since you can defer the decision until you know a good default/error message to put, there's no reason not to use it.

  • 8
    That means you get a compiler error if you don't handle the error. -- Well, that was the reasoning behind Checked Exceptions in Java, but we all know how well that worked out. – Robert Harvey Dec 1 '16 at 20:35
  • 4
    @RobertHarvey In a way, Java's checked exceptions were the poor man's version of this. Unfortunately they could be "swallowed" (like in the OP's example). They were also not real types, making them an extraneous additional path to the code flow. Languages with better type systems allow you to encode errors as first-class values, which is what you do in (say) Haskell with Maybe, Either, etc. Elm looks like it's taking a page from languages such as ML, OCaml or Haskell. – Andres F. Dec 1 '16 at 23:02
  • 3
    @JimmyJames No, they don't force you. You only have to "handle" the error when you want to use the value. If I do x = some_func(), I don't have to do anything unless I want to examine the value of x, in which case I can check whether I have an error or a "valid" value; moreover, it's a static type error to attempt to use one in place of the other, so I cannot do it. If Elm's types work anything like other functional languages, I can actually do stuff like compose values from different functions before I even know whether they are errors or not! This is typical of FP languages. – Andres F. Dec 1 '16 at 23:06
  • 6
    @atoth But you do have significant gains and there is a very good reason (as has been explained in multiple answers to your question). I truly encourage you to learn a language with an ML-like syntax and you'll see how liberating it is to get rid of C-like syntax cruft (ML, by the way, was developed in the early 70s, which makes it roughly a contemporary of C). The people who designed this kind of type systems consider this kind of syntax normal, unlike C :) While you are at it, it wouldn't hurt to learn a Lisp, too :) – Andres F. Dec 2 '16 at 15:09
  • 6
    @atoth If you want to take one thing from all of this, take this one: always make sure you don't fall prey to the Blub Paradox. Don't be irritated at new syntax. Maybe it's there because of powerful features you are unfamiliar with :) – Andres F. Dec 2 '16 at 15:17
10

In order to understand this statement, we first have to understand what a static type system buys us. In essence, what a static type system gives us, is a guarantee: iff the program type checks, a certain class of runtime behaviors cannot occur.

That sounds ominous. Well, a type checker is similar to a theorem checker. (Actually, per the Curry-Howard-Isomorphism, they are the same thing.) One thing that is very peculiar about theorems is that when you prove a theorem, you prove exactly what the theorem says, no more. (That's for example, why, when someone says "I have proven this program correct", you should always ask "please define 'correct'".) The same is true for type systems. When we say "a program is type-safe", what we mean is not that no possible error can occur. We can only say that the errors the type system promises us to prevent can't occur.

So, programs can have infinitely many different runtime behaviors. Of those, infinitely many ones are useful, but also infinitely many ones are "incorrect" (for various definitions of "correctness"). A static type system allows us to prove that a certain finite, fixed set of those infinitely many incorrect runtime behaviors cannot occur.

The difference between different type systems is basically in which, how many, and how complex runtime behaviors they can prove to not occur. Weak type systems such as Java's can only prove very basic things. For example, Java can prove that a method that is typed as returning a String cannot return a List. But, for example, it can not prove that the method won't not return. It can also not prove that the method won't throw an exception. And it cannot prove that it won't return the wrong String – any String will satisfy the type checker. (And, of course, even null will satisfy it as well.) There are even very simple things that Java cannot prove, which is why we have exceptions such as ArrayStoreException, ClassCastException, or everybody's favorite, the NullPointerException.

More powerful type systems like Agda's can also prove things like "will return the sum of the two arguments" or "returns the sorted version of the list passed as an argument".

Now, what the designers of Elm mean by the statement that they have no runtime exceptions is that Elm's type system can prove the absence of (a significant portion of) runtime behaviors that in other languages can not be proven to not occur and thus might lead to erroneous behavior at runtime (which in the best case means an exception, in a worse case means a crash, and in the worst case of all means no crash, no exception, and just a silently wrong result).

So, they are not saying "we don't implement exceptions". They are saying "things that would be runtime exceptions in typical languages that typical programmers coming to Elm would have experience with, are caught by the type system". Of course, someone coming from Idris, Agda, Guru, Epigram, Isabelle/HOL, Coq, or similar languages will see Elm as pretty weak in comparison. The statement is more aimed at typical Java, C♯, C++, Objective-C, PHP, ECMAScript, Python, Ruby, Perl, … programmers.

  • 5
    Note to potential editors: I am very sorry about the use of double and even triple negatives. However, I left them in on purpose: type systems guarantee the absence of certain kinds of runtime behaviors, i.e. they guarantee certain things not to occur. And I wanted to keep that formulation "prove to not occur" intact, which unfortunately leads to constructions like "it can't prove that a method won't not return". If you can find a way to improve these, go ahead, but please keep the above in mind. Thank you! – Jörg W Mittag Dec 2 '16 at 7:43
  • 2
    Good answer overall, but one small nitpick: "a type checker is similar to a theorem prover." Actually, a type checker is more akin to a theorem checker: they both do verification, not deduction. – gardenhead Mar 5 '17 at 2:00
4

Elm can guarantee no runtime exception for the same reason C can guarantee no runtime exception: The language does not support the concept of exceptions.

Elm has a way of signalling error conditions at runtime, but this system it is not exceptions, it is "Results". A function which may fail returns a "Result" which contains either a regular value or an error. Elms is strongly typed, so this is explicit in the type system. If a function always return an integer, it has the type Int. But if it either returns an integer or fails, the return type is Result Error Int. (The string is the error message.) This forces you to explicitly handle both cases at the call site.

Here is an example from the introduction (a bit simplified):

view : String -> String 
view userInputAge =
  case String.toInt userInputAge of
    Err msg ->
        text "Not a valid number!"

    Ok age ->
        text "OK!"

The function toInt may fail if the input is not parseable, so its return type is Result String int. To get the actual integer value, you have to "unpack" via pattern matching, which in turn forces you to handle both cases.

Results and exceptions fundamentally does the same thing, the important difference is the "defaults". Exceptions will bubble up and terminate the program by default, and you have to explicitly catch them if you want to handle them. Result is the other way - you are forced to handle them by default, so you have to expliitly pass them all the way to the top if you want them to terminate the program. It is easy to see how this behavor may lead to more robust code.

  • 2
    @atoth Here's an example. Imagine language A allows exceptions. Then you're given the function doSomeStuff(x: Int): Int. Normally you expect it to return an Int, but can it throw an exception as well? Without looking at its source code, you can't know. In contrast, a language B which encodes errors via types may have the same function declared like this: doSomeStuff(x: Int): ErrorOrResultOfType<Int> (in Elm this type is actually named Result). Unlike in the first case, it's now immediately obvious whether the function may fail, and you must handle it explicitly. – Andres F. Dec 1 '16 at 18:17
  • 1
    As @RobertHarvey implies in a comment on another answer, this seems to be basically like checked exceptions in Java. What I learned from working with Java in the early days when most exceptions were checked is that you really don't want to be forced to write code to always errors at the point they occur. – JimmyJames Dec 1 '16 at 21:52
  • 1
    @JimmyJames It's not like checked exceptions because exceptions don't compose, can be ignored ("swallowed") and are not first-class values :) I really recommend learning a statically typed functional language to truly understand this. This isn't some newfangled thing Elm made up -- this is how you program in languages such as ML or Haskell, and it's different from Java. – Andres F. Dec 1 '16 at 23:13
  • 2
    @AndresF. this is how you program in languages such as ML or Haskell In Haskell, yes; ML, no. Robert Harper, a major contributor to Standard ML and programming language researcher, considers exceptions to be useful. Error types can get in the way of function composition in cases where you can guarantee an error won't occur. Exceptions also have different performance. You don't pay for exceptions that aren't thrown, but you pay for checking error values every time, and exceptions are a natural way to express backtracking in some algorithms – Doval Dec 1 '16 at 23:34
  • 1
    @JimmyJames: actually, it's nothing at all like checked exceptions in Java. It is like Optional in Java 8, though! (Unfortunately, an Optional can still be null, but imagine Java 8 with Optional but without null.) – Jörg W Mittag Dec 2 '16 at 7:48
2

First, please note that your example of "swallowing" exceptions is in general a terrible practice and completely unrelated to having no run time exceptions; when you think about it, you did have a run time error, but you chose to hide it and do nothing about it. This will often result in bugs which are hard to understand.

This question could be interpreted in any number of ways, but since you mentioned Elm in the comments, the context is clearer.

Elm is, among other things, a statically typed programming language. One of the benefits of this kind of type systems is that many classes of errors (though not all) are caught by the compiler, before the program is actually used. Some kinds of errors can be encoded in types (such as Elm's Result and Task), instead of being thrown as exceptions. This is what the designers of Elm mean: many errors will be caught at compile time instead of at "run time", and the compiler will force you to deal with them instead of ignoring them and hoping for the best. It's clear why this is an advantage: better that the programmer becomes aware of a problem before the user does.

Note that when you don't use exceptions, errors are encoded in other, less surprising ways. From Elm's documentation:

One of the guarantees of Elm is that you will not see runtime errors in practice. NoRedInk has been using Elm in production for about a year now, and they still have not had one! Like all guarantees in Elm, this comes down to fundamental language design choices. In this case, we are helped by the fact that Elm treats errors as data. (Have you noticed we make things data a lot here?)

Elm designers are a bit bold in claiming "no run time exceptions", though they qualify it with "in practice". What they likely mean is "less unexpected errors than if you were coding in javascript".

  • Am I misreading it, or are they just playing a semantic game? They outlaw the name "run time exception", but then just replace it with a different mechanism that conveys error information up the stack. This sounds like just changing the implementation of an exception to a similar concept or object that implements error message differently. That is hardly earth-shattering. It is like any statically typed language. Compare switching from a COM HRESULT to a .NET exception. Different mechanism, but still is a run time exception, no matter what you call it. – Mike Dec 1 '16 at 17:12
  • @Mike To be honest I haven't looked at Elm in detail. Judging by the docs, they have types Result and Task which look very similar to the more familiar Either and Future from other languages. Unlike exceptions, values of these types can be combined and at some point you must explicitly handle them: do they represent valid value or an error? I don't read minds, but this lack of surprising the programmer is probably what Elm designers meant by "no run time exceptions" :) – Andres F. Dec 1 '16 at 17:22
  • @Mike I agree it's not earth-shattering. The difference with runtime exceptions is that they are not explicit in the types (i.e. you cannot know, without looking at its source code, whether a piece of code may throw); encoding errors in types is very explicit and prevents the programmer from overlooking them, leading to safer code. This is done by many FP languages and is indeed nothing new. – Andres F. Dec 1 '16 at 17:25
  • 1
    As per your comments I think there's more than "static type checks" to it. You can add it to JS using Typescript, which is a lot less constraining than a new "make-or-break" ecosystem. – atoth Dec 1 '16 at 17:52
  • 1
    @AndresF.: Technically speaking, the newest feature of Java's type system is Parametric Polymorphism, which hails form the late 60s. So, in a manner speaking, saying "modern" when you mean "not Java" is somewhat correct. – Jörg W Mittag Dec 2 '16 at 7:51
0

Elm claims:

No runtime errors in practice. No null. No undefined is not a function.

But you ask about runtime exceptions. There's a difference.

In Elm, nothing returns an unexpected result. You can NOT write a valid program in Elm that produces runtime errors. Thus, you don't need exceptions.

So, the question should be:

What is the benefit of having "no runtime errors"?

If you can write code that never has runtime errors, your programs will never crash.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.