Null pointer exceptions, also known as NPEs, are pretty common errors.
- In Java: java.lang.NullPointerException
- In Ruby: undefined method '...' for nil:NilClass
- In Python: AttributeError: 'NoneType' object has no attribute '...'
- In C#: Object reference not set to an instance of an object
- In C/C++: segmentation fault
Heck, two days ago I couldn’t buy a bus ticket because I got a nice “Object reference not set to an instance of an object” in the payment page.
The good news? Crystal doesn’t allow you to have null pointer exceptions.
Let’s start with the simplest example:
nil.fooCompiling the above program gives this error:
Error in foo.cr:1: undefined method 'foo' for Nil
nil.foo
^~~
nil, the only instance of the Nil class, behaves just like any other class in Crystal.
And since it doesn’t have a method named “foo”, an error is issued at compile time.
Let’s try with a slightly more complex, but made up, example:
class Box
getter :value
def initialize(value)
@value = value
end
end
def make_box(n)
case n
when 1, 2, 3
Box.new(n * 2)
when 4, 5, 6
Box.new(n * 3)
end
end
n = ARGV.size
box = make_box(n)
puts box.valueCan you spot the bug?
Compiling the above program, Crystal says:
Error in foo.cr:20: undefined method 'value' for Nil
puts box.value
^~~~~
================================================================================
Nil trace:
foo.cr:19
box = make_box n
^
foo.cr:19
box = make_box n
^~~~~~~~
foo.cr:9
def make_box(n)
^~~~~~~~
foo.cr:10
case n
^
Not only it tells you that you might have a null pointer exception (in this case, when n is not one of 1, 2, 3, 4, 5, 6),
but it also shows you where the nil originated. It’s in the case expression, which has a default empty else clause, which has a nil value.
One last example, which might well be real code:
require "socket"
# Create a new TCPServer at port 8080
server = TCPServer.new(8080)
# Accept a connection
socket = server.accept
# Read a line and output it capitalized
puts socket.gets.capitalizeCan you spot the bug now? It turns out that TCPSocket#gets
(IO#gets, actually),
returns nil at the end of the file or, in this case, when the connection is closed.
So capitalize might be called on nil.
And Crystal prevents you from writing such a program:
Error in foo.cr:10: undefined method 'capitalize' for Nil
puts socket.gets.capitalize
^~~~~~~~~~
================================================================================
Nil trace:
std/file.cr:35
def gets
^~~~
std/file.cr:40
size > 0 ? String.from_cstr(buffer) : nil
^
std/file.cr:40
size > 0 ? String.from_cstr(buffer) : nil
^
To prevent this error, you can do the following:
require "socket"
server = TCPServer.new(8080)
socket = server.accept
line = socket.gets
if line
puts line.capitalize
else
puts "Nothing in the socket"
endThis last program compiles fine. When you use a variable in an if’s condition, and because the only
falsy values are nil and false, Crystal knows that line can’t be nil inside the “then” part of the if.
This is both expressive and executes faster, because it’s not needed to check for nil values at runtime at every method call.
To conclude this post, one last thing left to say is that while porting the Crystal parser from Ruby to Crystal, Crystal refused to compile because of a possible null pointer exception. And it was correct. So in a way, Crystal found a bug in itself :-)
Null pointer exceptions, also known as NPEs, are pretty common errors.
Heck, two days ago I couldn’t buy a bus ticket because I got a nice “Object reference not set to an instance of an object” in the payment page.
The good news? Crystal doesn’t allow you to have null pointer exceptions.
Let’s start with the simplest example:
Compiling the above program gives this error:
Error in foo.cr:1: undefined method 'foo' for Nil nil.foo ^~~nil, the only instance of the Nil class, behaves just like any other class in Crystal. And since it doesn’t have a method named “foo”, an error is issued at compile time.Let’s try with a slightly more complex, but made up, example:
Can you spot the bug?
Compiling the above program, Crystal says:
Error in foo.cr:20: undefined method 'value' for Nil puts box.value ^~~~~ ================================================================================ Nil trace: foo.cr:19 box = make_box n ^ foo.cr:19 box = make_box n ^~~~~~~~ foo.cr:9 def make_box(n) ^~~~~~~~ foo.cr:10 case n ^Not only it tells you that you might have a null pointer exception (in this case, when n is not one of 1, 2, 3, 4, 5, 6), but it also shows you where the
niloriginated. It’s in thecaseexpression, which has a default emptyelseclause, which has anilvalue.One last example, which might well be real code:
Can you spot the bug now? It turns out that TCPSocket#gets (IO#gets, actually), returns
nilat the end of the file or, in this case, when the connection is closed. Socapitalizemight be called onnil.And Crystal prevents you from writing such a program:
Error in foo.cr:10: undefined method 'capitalize' for Nil puts socket.gets.capitalize ^~~~~~~~~~ ================================================================================ Nil trace: std/file.cr:35 def gets ^~~~ std/file.cr:40 size > 0 ? String.from_cstr(buffer) : nil ^ std/file.cr:40 size > 0 ? String.from_cstr(buffer) : nil ^To prevent this error, you can do the following:
This last program compiles fine. When you use a variable in an
if’s condition, and because the only falsy values arenilandfalse, Crystal knows thatlinecan’t be nil inside the “then” part of theif.This is both expressive and executes faster, because it’s not needed to check for
nilvalues at runtime at every method call.To conclude this post, one last thing left to say is that while porting the Crystal parser from Ruby to Crystal, Crystal refused to compile because of a possible null pointer exception. And it was correct. So in a way, Crystal found a bug in itself :-)