Skip to content

Latest commit

 

History

History
156 lines (122 loc) · 4.89 KB

noreturn.md

File metadata and controls

156 lines (122 loc) · 4.89 KB
id title
noreturn
T.noreturn

The type T.noreturn is an "impossible" type: zero values in the language have this type. This type has a number of key use cases.

Declaring that a method always raises

T.noreturn can declare that a method never returns (for example, because it unconditionally raises).

sig {returns(T.noreturn)}
def raise_always
  raise RuntimeError
end

sig {returns(T.noreturn)}
def exit_program
  exit
end

In fact, neither raise nor exit are keywords in Ruby but are in fact methods (defined on the Kernel module in the standard library). Sorbet uses an RBI file to declare that raise and exit never returns, among others.

Detecting unreachable code

T.noreturn powers Sorbet's unreachable (dead) code analysis, because it is impossible to have a value of type T.noreturn. For example:

sig {params(x: T.nilable(Integer)).void}
def example(x)
  if x.nil?
    raise ""
    puts(x) # error: This code is unreachable
  end
end

The call to puts(x) happens after the call to raise. Since Sorbet knows that raise "" evaluates to T.noreturn, it knows that the call to puts(x) will never run. (The error message makes no mention of T.noreturn explicitly, but this is in fact what powers the underlying analysis.)

Note that it can be useful to assert that a given branch of control flow must be unreachable. For these cases, Sorbet provides T.absurd, which also handles exhaustiveness checking (a special case of asserting that a branch of code is unreachable).

Writing infinite loops

Another way a method can never return is if it infinitely loops, either by using loop {...} or by using a while loop with an unconditionally true condition, neither of which ever break out of the loop:

sig {returns(T.noreturn)}
def loop_forever
  loop do
    # ...
  end
end

sig {returns(T.noreturn)}
def while_forever
  while true
    # ...
  end
end

Infinite loops like this occur frequently in long-lived processes that are designed to respond to user input indefinitely.

Having type system support for this may not seem immediately useful at first, but ties into the previous section: Sorbet can detect code written after an infinite loop, and flag that the code is unreachable.

Detecting impossible-to-call methods

Because of intersection types, it is possible to create types for which there cannot be any values. If there are no values of a given type, and that type is used for an argument of a method, Sorbet flags the usage as dead code:

class A; end
class B; end

sig {params(x: T.all(A, B)).void}
def example(x)
  puts(x) # error: This code is unreachable
end

As discussed in Understanding how intersection types collapse, T.all(A, B) collapses to T.noreturn because the classes A and B are completely unrelated, and it therefore impossible for a value to simultaneously be an instance of A and B.

In these examples, Sorbet has caught an example of a method which is not possible to call.

T.noreturn and subtyping

T.noreturn is a subtype of all other types. This can be useful in certain situations. For example, the type of the empty array is most accurately typed as T::Array[T.noreturn]. Because T.noreturn is a subtype of all other types, this means that [] has type T::Array[Integer] and T::Array[String] and T::Array[T.any(Integer, String)], etc.

This also allows Sorbet to detect when, for example, there's an incorrect attempt to access the first element of a known-empty list:

xs = T::Array[T.noreturn].new
x = xs.fetch(0)
puts(x) # error: This code is unreachable

T.noreturn and T.untyped

Despite what we've claimed throughout this page, technically it is possible to have a value of type T.noreturn by way of T.untyped:

sig {returns(T.noreturn)}
def always_raises
  T.unsafe("Just kidding, it actually does return")
end

This is an unavoidable downside of Sorbet's use of T.untyped to implement a gradual type system.

This is a major reason why Sorbet's runtime type checking is valuable: in order for Sorbet's static dead code analysis to be accurate, Sorbet relies on the fact that static violations of the declared types will be caught at runtime by checking method signatures and type assertions.

Disabling runtime checks will not change anything about what Sorbet infers statically--in particular, Sorbet will still believe it is impossible to have a value of type T.noreturn and will still report the same unreachable code errors it normally would.