Skip to content

Latest commit

 

History

History
233 lines (166 loc) · 7.43 KB

troubleshooting.md

File metadata and controls

233 lines (166 loc) · 7.43 KB
id title
troubleshooting
Troubleshooting

This is one of three docs aimed at helping answer common questions about Sorbet:

  1. Troubleshooting (this doc)
  2. Frequently Asked Questions
  3. Sorbet Error Reference

This doc covers two main topics:

Validating our assumptions

When faced with a type error, checking our assumptions is step number one. The first question to ask is:

Are my files # typed: false or # typed: true?

There are also some tools for helping debug type-related errors:

T.reveal_type

If we wrap a variable or method call in T.reveal_type, Sorbet will show us what type it thinks that variable has in the output of srb tc. This is a super powerful debugging technique! T.reveal_type should be one of the first tools to reach for when debugging a confusing error.

Try making a hypothesis of what the problem is ("I think the problem is...") and then create small examples in https://sorbet.run to test the hypothesis with T.reveal_type. For example:

# typed: true
extend T::Sig

sig {params(xs: T::Array[Integer]).returns(Integer)}
def foo(xs)
  T.reveal_type(xs.first) # => Revealed type: `T.nilable(Integer)`
end
→ View on sorbet.run

With this example we see that xs.first returns T.nilable(Integer).

Frequently when troubleshooting type errors, either something is nil or T.untyped unexpectedly. We can use T.reveal_type to track down where the type originated.

Remember: Sorbet is a gradual type system. Even if most code in a codebase is typed, it's still possible for code to be untyped!

sorbet.run

Sorbet is available in an online sandbox:

→ sorbet.run

The online version of Sorbet can typecheck...

  • core Ruby language constructs (like control flow)
  • Ruby standard libraries (like Array and Hash)
  • select DSLs (like T::Struct props)

Using sorbet.run to make a minimal repro is a great way to isolate whether something is "just how Sorbet works" or is acting strangely in conjunction with the code in a specific project locally.

Note: sorbet.run only shows how the static component of Sorbet works. To test the runtime component, see the Quick Reference.

Run the code

Types predict values. Is Sorbet's prediction accurate? Try using a Ruby REPL (like irb or binding.pry) to run code. Does it actually work? If it doesn't actually work, Sorbet caught a bug!

Otherwise, there are a handful of reasons why Sorbet predicts code will not work even when it does:

  • Maybe the code ran fine for all provided input values, but there's an edge case being left untested.

  • Maybe the code uses a method from the standard library that is missing or typed incorrectly (our standard library shims are sometimes incomplete).

  • Maybe a method Sorbet thinks doesn't exist actually does exist because it was dynamically defined with define_method or missing_method. See Escape Hatches below for working around this.

*.rbi files & missing methods

One of the most confusing errors from Sorbet can be "7003: Method does not exist." Ruby is a very dynamic language, and methods can be defined in ways Sorbet cannot see statically. It's possible that even though a method exists at runtime, Sorbet cannot see it.

However, we can use *.rbi files to declare methods to Sorbet so that it can see them statically. For more information about RBI files:

→ RBI files

Help with common errors

The tips above are very generic and apply to lots of cases. For some common gotchas when using Sorbet, here are two more specific resources:

  • Sorbet Error Reference

    This document is a collaborative reference with suggestions for commonly encountered error codes. You should see links to this document in Sorbet's error output.

  • FAQ

    This is a list of questions people commonly have when working with Sorbet and the runtime type system. Skim it to see if it says anything useful!

Escape Hatches

Regardless of whether we can figure out the root cause of the error at hand, Sorbet is designed as a gradual type system. This means there will always be escape hatches to silence the problem.

T.unsafe

By wrapping an expression like x in T.unsafe(...) we can ask Sorbet to forget the result type of something.

One case when this is useful is when we know for sure that a method exists, but Sorbet doesn't know that method exists statically:

# typed: true
class A
  def method_missing(method)
    puts "Called #{method}"
  end
end

A.new.foo # error: Method `foo` does not exist on `A`

In cases like this, we have a couple options. The first one is just: rewrite the code. Code that's hard for Sorbet to understand is frequently hard for developers to understand. By rewriting confusing code, we benefit all future readers.

However, in the case when we're sure that we don't want to refactor the code, we have an escape hatch: T.unsafe. It looks like this:

# typed: true
class A
  def method_missing(method)
    puts "Called #{method}"
  end
end

T.unsafe(A.new).foo # => ok

The T.unsafe call effectively says to Sorbet, "trust me, I know what I'm doing." At this point, the burden of correctness shifts from Sorbet to the programmer.

T.unsafe(self)

Note that the call to T.unsafe must wrap the receiver of the method call. In this example:

# typed: true
class A
  def method_missing(method)
    puts "Called #{method}"
  end

  def initialize
    foo # error: Method `foo` does not exist on `A`
  end
end

foo is a method that's being called without on the implicit receiver of self. Another way of saying that is that foo is the same as self.foo in Ruby. So to use unsafe to silence this error, we have to make the self.foo explicit:

# typed: true
class A
  def method_missing(method)
    puts "Called #{method}"
  end

  def initialize
    T.unsafe(self).foo # ok
  end
end

T.cast: a safer alternative to T.unsafe

T.unsafe is maximally unsafe. It forces Sorbet to forget all type information statically---sometimes this is more power than we need. For the cases where we the programmer know of an invariant that isn't currently expressed in the type system, T.cast is a good middle-ground.

Changing when runtime exceptions are raised

Both T.unsafe and T.cast relax Sorbet's static checks. They don't silence any exceptions that would be raised for type errors at runtime.

There are also ways to change the runtime behavior; these escape hatches are documented elsewhere. See:

  • Enabling Runtime Checks, which has docs on how to configure and opt out of certain runtime checks.

  • Runtime Configuration, which has docs on how to write low-level configuration handlers that intercept any exceptions that the runtime could raise.