id | title | sidebar_label |
---|---|---|
error-reference |
Sorbet Error Reference |
Error Reference |
Heads up: There aren't any docs yet for this error code. If you have suggestions for what would have helped you solve this problem, click the "Edit" button above to contribute! Otherwise, try using the search above to find an answer.
Note: This list is not exhaustive! Some errors are very context dependent and other error codes are not common enough to know how to generally suggest help. Contributions to this list are welcome!
This is one of three docs aimed at helping answer common questions about Sorbet:
This page contains tips and tricks for common errors from srb
.
Sorbet has crashed. Please report an issue!
Sorbet couldn't find a file.
There was a Ruby syntax error. Sorbet was unable to parse the source code. If you encounter this error but your code is accepted by Ruby itself, this is a bug in our parser; please report an issue to us so we can address it.
The only intentional break with Ruby compatibility is that method names that are keywords have some limitations with multi-line code, as explained in #1993, and should not be reported.
Sorbet requires that every include
references a constant literal. For example,
this is an error, even in # typed: false
files:
module A; end
module B; end
def x
rand.round == 0 ? A : B
end
class Main
include x # error: `include` must be passed a constant literal
end
Non-constant literals make it hard to impossible to determine the complete inheritance hierarchy in a codebase. Sorbet must know the complete inheritance hierarchy of a codebase in order to check that a variable is a valid instance of a type.
It is possible to silence this error, but it should be done with utmost
caution, as Sorbet will fail in strange ways and make far less accurate
predictions about a codebase. To silence this error, use T.unsafe
:
module A; end
module B; end
def x
rand.round == 0 ? A : B
end
class Main
T.unsafe(self).include x
end
There are multiple definitions for a method. Having multiple definitions for a method is problematic, because it can make a codebase's behavior dependent on the order in which files are loaded.
The only way to silence this error currently is to mark the offending file as
# typed: false
.
There are multiple definitions for the same type member within a given class or module.
class Main
extend T::Generic
Elem = type_member
Elem = type_member # error: Duplicate type member
end
You can fix this by removing the second definition of the type member:
class Main
extend T::Generic
Elem = type_member # ok
end
This error usually comes when a class or module is dynamically defined and stored into a constant, like this:
A = ...
A::B = 1
where ...
is some expression which computes a class or module. Sorbet can't
know statically what this ...
code does (and for example even if could assume
that it's defining a class, Sorbet can't know what methods or constants it has).
Therefore, Sorbet does not support this pattern.
This means that the typechecker has been unable to resolve a reference to a constant (e.g., a Ruby class). Most commonly, this indicates that there's a typo.
First, try confirming whether the code runs successfully. Does the code raise an "uninitialized constant" error when run? If so, Sorbet caught a bug! Try finding out why that constant is actually uninitialized.
If it isn't a typo, then there are a few other things to look at. If it looks like the constant is related to a gem, maybe one of these helps:
-
Is it coming from a gem? Sorbet does not look through the gem's source code. Instead, there must be an
*.rbi
file for this gem. Try finding the*.rbi
corresponding to this gem, and searching through it for the constant.For more information, see RBI files. If you are at Stripe, please instead see http://go/types/rbi.
-
If the gem was recently updated, its
*.rbi
might need to be regenerated. Each RBI file has a line at the top which can be copy / pasted to re-generate the file when the underlying gem has changed. -
When deleting constants, sometimes they are still referenced from an autogenerated
*.rbi
file. If that's the case, consider deleting the constant or regenerating the file.
Another thing it could be: Sorbet explicitly does not support resolving constants through ancestors (both mixins or superclasses).
Concretely, here's an example of code rejected and accepted by Sorbet:
class Parent
MY_CONST = 91
end
class Child < Parent; end
Child::MY_CONST # error
Parent::MY_CONST # ok
Alternatively, if it's much more preferable to access the constant on the child, we can set up an explicit alias:
class Parent
MY_CONST = 91
end
class Child < Parent
MY_CONST = Parent::MY_CONST
end
Child::MY_CONST # ok
Parent::MY_CONST # ok
A class or instance variable is defined in the wrong context.
# typed: true
class A
extend T::Sig
def foo
@@class_var = T.let(10, Integer)
@x = T.let(10, Integer)
end
end
There are two such errors in the above. In the first, @@class_var
is declared
outside of the class scope. In the second, @x
is declared outside of the
initialize
method.
For how to fix, see Type Annotations.
An instance variable has been redeclared with another type.
# typed: true
class A
extend T::Sig
def initialize
@x = T.let(10, Integer)
@x = T.let("x", String)
end
end
A class was defined as the subclass of a type_alias
. It also occurs if a
type_alias
mixin is used in a class.
# typed: true
A = T.type_alias {Integer}
class B < A; end # error: Superclasses and mixins may not be type aliases
module M; end
AliasModule = T.type_alias {M}
class C
include AliasModule # error: Superclasses and mixins may not be type aliases
end
A class inherits from itself either directly or through an inheritance chain.
class A < A; end
class B < C; end
class C < B; end
A class was changed to inherit from a different superclass.
class A; end
class B; end
class C < A; end
class C < B; end
A class or instance variable declaration used T.cast
when it should use
T.let
.
class A
@@x = T.cast(10, Integer)
end
For how to fix, see Type Annotations.
Given code like this:
# typed: true
class Parent
extend T::Generic
Foo = type_member
end
class Child < Parent
extend T::Generic
end
We need to change our code to redeclare the type member in the child class too:
# typed: true
class Parent
extend T::Generic
Foo = type_member
end
class Child < Parent
extend T::Generic
Foo = type_member
end
The same thing holds for type templates.
Some modules require specific functionality in the receiving class to work. For
example Enumerable
needs a each
method in the target class.
Failing example in sorbet.run:
class Example
include Enumerable
end
To fix this, implement the required abstract methods in your class to provide the required functionality.
Passing example in sorbet.run:
class Example
include Enumerable
def each(&blk)
end
end
In # typed: strict
files, Sorbet requires that all constants are annotated
with a T.let
.
For how to fix, see Type Annotations.
Sorbet does not support creating normal Ruby constant aliases to type aliases. Once a type alias is created, all subsequent aliases must also be type aliases.
Concretely, this is not allowed:
A = T.type_alias {Integer}
B = A # error: Reassigning a type alias is not allowed
while this is:
A = T.type_alias {Integer}
B = T.type_alias {A}
(Why? This is due to design tradeoffs to enforce stronger internal invariants. Basically, Sorbet can emit more reliable warnings when users declare their intent to create a new type alias.)
See 5014. 5036 is the same error as 5014 but slightly modified
to allow more common Ruby idioms to pass by in # typed: true
(5036 is only
reported in # typed: strict
).
Sorbet does not allow inheriting from a class which inherits from T::Struct
.
class S < T::Struct
prop :foo, Integer
end
class Bad < S; end # error
This limitation exists because, in order to generate a static type for
initialize
for a struct, we need to know all of the prop
s that are declared
on this struct. By disallowing inheritance of structs, we can know that all of
the props declared on this struct were syntactically present in the class body.
One common situation where inheritance may be desired is when a parent struct declares some common props, and children structs declare their own props.
class Parent < T::Struct
prop :foo, Integer
end
class ChildOne < Parent # error
prop :bar, String
end
class ChildTwo < Parent # error
prop :quz, Symbol
end
We can restructure the code to use composition instead of inheritance.
class Common < T::Struct
prop :foo, Integer
end
class ChildOne < T::Struct
prop :common, Common
prop :bar, String
end
class ChildTwo < T::Struct
prop :common, Common
prop :quz, Symbol
end
A class or module tried to inherit, include, or extend a final class or module.
class Final
extend T::Helpers
final!
end
class Bad < Final; end # error
A class or module was declared as final, but a method in the class or module was
not explicitly declared as final with a final sig
.
class C
extend T::Helpers
final!
def no_sig; end # error
extend T::Sig
sig {void}
def non_final_sig; end # error
sig(:final) {void}
def final_sig; end # good
end
Use of implementation
has been replaced by override
.
Static methods (like self.foo
) can never be mixed into another class or
module. Both include
and extend
only mix that module's instance methods
onto the target class or module. Classes can inherit static methods from their
superclass, but only classes (not modules) can be superclasses.
Thus, a static, abstract method on a module is impossible to implement, and thus is a no-op.
module MyMixin
sig {abstract.void}
def foo; end
sig {abstract.void}
def self.bar; end # error: Static methods in a module cannot be abstract
end
Some alternatives:
-
Use
mixes_in_class_methods
, which declares to Sorbet that when a module is included, some other module should be extended into the target class. Full documentation here. This is the preferred option. -
Separate the interface into two modules. Include one and extend the other in all the places where
-
Change the abstract module to an abstract class, and update all downstream references to inherit from this class instead of including the original module.
In # typed: strict
files, Sorbet requires that all instance and class
variables are annotated with a T.let
.
For how to fix, see Type Annotations.
Sorbet does not allow reassigning a variable to a different type within a loop or block. (Note that we model blocks similarly to loops, as in general they may execute 0, 1, or more times). Due to implementation constraints, Sorbet does not permit this behavior.
A prototypical example of code that might trigger this is code that sets a
variable to nil
, and then updates it if some value is found inside a loop:
found = nil
list.each do |elem|
found = elem if want?(elem)
end
In most cases, we can fix this error by declaring the type of the loop variable
outside the loop using T.let
:
# This is a type annotation that explicitly widens the type:
found = T.let(nil, T.nilable(String))
list.each do |elem|
found = elem if want?(elem)
end
But my variable does not change its type, it is always a Boolean
!
In Ruby, there is no Boolean
type. Instead, there are FalseClass
and
TrueClass
types, the union of which defines
T::Boolean
type as a union type.
When Sorbet encounters a variable declaration like x = true
, it infers the
type of x
as TrueClass
. An assignment to x
later on in the same block such
as x = false
would imply that the variable is reassigned to a different type
(namely, to FalseClass
in this case).
For this reason, a loop such as the following triggers an error:
# Declares `found_valid` with type `FalseClass`
found_valid = false
list.each do |elem|
# Might change the type of `found_valid` to `TrueClass`
found_valid = true if valid?(elem) # error: Changing the type of a variable in a loop
end
The fix, again, is to use T.let
to widen the type to T::Boolean
:
# Declare `found_valid` with type `T::Boolean`
found_valid = T.let(false, T::Boolean)
list.each do |elem|
# Does not change the type of `found_valid`
found_valid = true if valid?(elem) # ok
end
The generated
annotation in method signatures is deprecated.
For alternatives, see Enabling Runtime Checks which talks about how to change the runtime behavior when method signatures encounter a problem.
This is a standard type mismatch. A method's sig
declares one type, but the
actual value didn't match. For example:
'str' + :sym # error: Expected `String` but found `Symbol(:"sym")` for argument `arg0`
Even still, sometimes these errors can be rather confusing. Consider using
T.reveal_type
to pin down the origin of why Sorbet
thinks the types are what it says.
Why does Sorbet think this is nil
? I just checked that it's not!
That's a great question, and probably the most common question people have when using Sorbet!
It's answered here: Limitations of flow-sensitivity
This error indicates a call to a method we believe does not exist (a la Ruby's
NoMethodError
exception). Some steps to debug:
-
Double check that the code actually runs, either in the REPL, in CI, or with manual tests. If the method doesn't actually exist when run, Sorbet caught a bug!
-
Even if the method exists when run, Sorbet still might report an error because the method won't always be there. For example, maybe the value is nilable, or we have a union of a handful of different types.
-
Many times, methods are defined dynamically in Ruby. Sorbet cannot see methods defined with
define_method
. Sorbet also can't see methods defined using Ruby'sincluded
+other.extend(self)
pattern. For such dynamically defined methods, Sorbet requires*.rbi
files which define the method statically.See the RBI docs for how to regenerate the
*.rbi
files. -
Sorbet will complain about this code:
module MyModule; end sig {params(x: MyModule).void} def foo(x) x.nil? # error: Method `nil?` does not exist on `MyModule` end
The
nil?
method is defined onKernel
in Ruby.Kernel
is included inObject
(which classes default to inheriting from), but not onBasicObject
(which classes can optionally inherit from).The solution is to
include Kernel
in our module:module MyModule include Kernel end
-
Sorbet will complain about this code:
module MyModule def foo; puts 'hello'; end end
The issue is similar to the above:
puts
is defined onKernel
, which is not necessarily included in our module. For this situation, there are actually two fixes:# Option 1: include Kernel module MyModule include Kernel def foo; puts 'hello'; end end
# Option 2: Kernel.puts module MyModule def foo; Kernel.puts 'hello'; end end
This error indicates that a method has been called with incorrect parameters. There are a few cases where this can occur:
- Too many parameters
- Not enough parameters
- Trying to pass parameters that don't exist
- Missing required parameters
- Positional parameters used when the method expects named parameters, and vice versa
def foo(x: nil); end
foo(1) # error
foo(y: 1) # error
foo(x: 1) # ok
foo() # ok
def bar(x:); end
bar() # error
bar(1) # error
bar(x: 1) # ok
In Sorbet, it is an error to have provably unreachable code. Because Sorbet is sensitive to control flow in a program, Sorbet can not only track what types each variable has within all the branches of a conditional, but also whether any given branch could be executed at all.
Erroring for dead or unreachable code is generally a way to prevent bugs. People
don't usually expect that some branch of code is never taken; usually dead code
errors come from simple typos or misunderstandings about how Ruby works. In
particular: the only two "falsy" values in Ruby are nil
and false
.
Note if you intend for code to be dead because you've exhausted all the cases
and are trying to raise in the default case, use T.absurd
to assert that a
case analysis is exhaustive. See Exhaustiveness Checking
for more information.
Sometimes, dead code errors can be hard to track down. The best way to pinpoint
the cause of a dead code error is to wrap variables or expressions in
T.reveal_type(...)
to validate the assumptions that a piece of code is making.
For more troubleshooting tips, see Troubleshooting.
If for whatever reason it's too hard to track down the cause of a dead code
error, it's possible to silence it by making a variable or expression
"unanalyzable," aka untyped. (When something is untyped, Sorbet will do very
limited flow-sensitivity analysis compared to if Sorbet knows the type. To make
something unanalyzable, we can wrap it in T.unsafe(...)
:
x = false
if x
puts 'hello!' # error: This code is unreachable
end
if T.unsafe(x)
puts 'hello!' # ok
end
In this (contrived) example, Sorbet knows statically that x
is always false
and so our puts
within the first if
is never reachable. On the other hand,
Sorbet allows the second if
because we've explicitly made x
unanalyzable
with T.unsafe(...)
. T.unsafe is one of a handful of
escape hatches built into Sorbet.
Sorbet has a special method called T.reveal_type
which can be useful for
debugging. T.reveal_type(expr)
will report an error in the output of srb tc
that shows what the static component of Sorbet thinks the result type of expr
is.
Making this an error is nice for two reasons:
-
It makes our internal implementation easier 😅 We don't have some special-case messages and then error messages. The only thing Sorbet prints under normal circumstances are error messages.
-
It serves as a reminder to remove
T.reveal_type
before committing a change. Since it's a proper error, Sorbet will exit with non-zero status until it's removed.
For more information, see Troubleshooting.
Looking for how to assert that an expression has a certain type? Check out Type Assertions.
In typed: strict files, Sorbet requires that all methods are annotated with a
sig
. In a # typed: true
file Sorbet implicitly assumes that definitions
without types are T.untyped
, but in a # typed: strict
file, Sorbet will no
longer make this implicit assumption.
You can still add a sig
which declares the arguments or return as T.untyped
,
so # typed: strict
does not outright ban T.untyped
. The upside is that usage
of T.untyped
is more explicit, which makes it easier to drive the number of
occurrences down. If you're seeing this warning, there's no time like the
present to add proper types to your public-facing API (i.e., your top-level
constant and method definitions)!
For how to fix, see Method Signatures.
At typed: strong
, Sorbet no longer allows T.untyped
as the intermediate
result of any method call. This effectively means that Sorbet knew the type
statically for 100% of calls within a file. This sigil is rarely used—usually
the only files that are # typed: strong
are RBI files and files with empty
class definitions. Most Ruby files that actually do interesting things will have
errors in # typed: strong
.
Sorbet does not have great support for splats right now.
In general, when considering taking a variable number of arguments, consider instead taking a single argument that's an array instead of a "rest" arg:
# ----- AVOID THIS ----------------------------
sig {params(xs: Integer).void}
def foo(*xs); end
xs = Array.new(3) {|i| i}
foo(*xs)
# ---------------------------------------------
# ----- Do this instead -----------------------
sig {params(ys: T::Array[Integer]).void}
def bar(ys); end
ys = Array.new(3) {|i| i}
bar(ys)
# ---------------------------------------------
If it is not possible to refactor the code, the current work around is to use
T.unsafe
:
# ----- WORST CASE ----------------------------
# Prefer the solution described above
sig {params(xs: Integer).void}
def foo(*xs); end
xs = Array.new(3) {|i| i}
T.unsafe(self).foo(xs)
# ---------------------------------------------
The method called declares a block parameter that is not T.nilable
, but a
block was not passed when it was called.
This can be fixed by either passing a block to the method, or changing the
method's signature for the block parameter from T.proc...
to
T.nilable(T.proc...)
(and then changing the method to deal with a nilable
block parameter).
This error occurs when a method is passed in a Proc
object that Sorbet does
not know the arity for statically.
One instance where this can happen is when using method
, since the arity of
method corresponding to the symbol is unknown. This can be fixed by passing in a
block with the correct arity:
# typed: strict
extend T::Sig
sig {params(blk: T.proc.params(arg0: String).void).void}
def foo(&blk)
end
# ----- AVOID THIS ----------------------------
foo(&method(:puts))
# ---------------------------------------------
# ----- Do this instead -----------------------
foo do |arg0|
method(:puts).call(arg0)
end
# ---------------------------------------------
This error occurs when a generic argument is passed in as a block parameter.
In # typed: strict
files, using a parameter from a method that does not have a
signature will cause this issue to be reported. Adding a signature to the method
will fix the issue.
# typed: strict
extend T::Sig
# ----- This will error -----------------------
def foo(&blk)
proc(&blk)
end
# ---------------------------------------------
# ----- This will not error -------------------
sig {params(blk: T.untyped).returns(T.untyped)}
def bar(&blk)
proc(&blk)
end
# ---------------------------------------------
Sorbet detected that it was possible for T.absurd
to be reached. This usually
means that something that was meant to cover all possible cases of a union type
did not cover all the cases.
See Exhaustiveness Checking for more information.
<script src="/js/error-reference.js"></script>