# naught generates Null Object classes so Ruby code stops guarding every call

> The naught gem builds null object classes at runtime from a small configuration block, giving you a logger that logs nothing, a black hole that swallows any chain, and objects that mimic an existing interface. The pattern is old, but the ergonomics of generating the class are what the gem adds.

**avdi/naught** — A toolkit for building Null Object classes in Ruby

- Repository: https://github.com/avdi/naught
- Stars: 1,059 · Forks: 47
- Language: Ruby
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/avdi-naught

## The problem naught removes is the guard clause on every call

The example that opens the documentation is a method that is not sure of itself. A Geordi class takes a logger that may be nil and guards three calls with a truthiness check:

```ruby
class Geordi
  def make_it_so(logger = nil)
    logger && logger.info("Reversing the flux phase capacitance!")
    logger && logger.info("Bounding a tachyon particle beam off of Data's cat!")
    logger && logger.warn("Warning, bogon levels are rising!")
  end
end
```

The null object pattern replaces the optional argument with an object that always exists and whose methods do nothing. The documentation shows the same method with a NullLogger whose debug, info, warn, error and fatal are all no-op methods, and the caller no longer has to think about whether a logger was passed. The stated benefit is that Null Objects make your code more confident, and the measurable effect is the removal of the && operators from every call site.

The project is upfront that the pattern itself is trivial. The documentation says plainly that you do not need a null object library, because the pattern is very simple at its core. What the gem adds is convenience and, more importantly, the ability to generate several different flavours of null object from a consistent configuration instead of hand writing each one.

The audience is Ruby developers who keep writing the same five line null class in several projects, and who want the more exotic variants, the black hole and the mimicking objects, without reimplementing them.

## Naught.build returns a class, not an instance

The entry point is a single method. You require the library, call Naught.build, and assign the result to a constant:

```ruby
require "naught"

NullObject = Naught.build

null = NullObject.new
null.foo                        # => nil
null.bar                        # => nil
```

The build call returns a class, and you instantiate it yourself, which is the part that trips people up at first. The documentation even pauses on it, asking what the build business is about, and the answer is that naught is a toolkit for building null object classes rather than a one size fits all solution. The configuration block passed to build is what customises the generated class, and internally naught uses the Builder pattern to make that work.

That structure has a practical consequence for a codebase. The generated class is named by you, assigned to a constant, and behaves like any other class. You can subclass it, reopen it, or pass it around as a dependency, and a test can assert that it was received. That is a meaningful difference from reaching for a global singleton, and it is the reason the gem leans on a builder rather than exposing a ready made object.

The methods the generated class responds to are not defined one by one, they are handled centrally, which is what makes a single call to build produce an object that answers nil to any message.

## The black hole variant returns itself for infinite chaining

A plain null object answers nil to every message, which means the chain stops at the first call. The documentation asks for something more: a black hole null object that supports infinite chaining of methods. The configuration is one line, config.black_hole:

```ruby
require "naught"

BlackHole = Naught.build do |config|
  config.black_hole
end

null = BlackHole.new
null.foo                           # => <null>
null.foo.bar.baz                   # => <null>
null << "hello" << "world"         # => <null>
```

The difference is visible in the output. Instead of nil, the black hole answers itself, so a chain of any length continues and ends at a black hole object rather than at nil. The << example matters more than it looks, because a null object that answers nil to << would raise on the second call in the chain, while a black hole keeps accepting the message and returns itself.

This is the shape you want when the code under test or the code under integration does not care about the return value and you do not want a NoMethodError to be raised on nil. It is also the shape you want to be careful with, because the object never stops the chain. There is no natural end point and no error to catch, which is the same strength and the same risk in one design.

## Explicit and implicit conversions to zero values

Some Ruby code paths check the type of a value rather than using it, and naught has a configuration for each case. The explicit conversions turn a null object into something that behaves like an empty or zero value:

```ruby
require "naught"

NullObject = Naught.build do |config|
  config.define_explicit_conversions
end

null = NullObject.new

null.to_s                          # => ""
null.to_i                          # => 0
null.to_f                          # => 0.0
null.to_a                          # => []
null.to_h                          # => {}
null.to_c                          # => (0+0i)
null.to_r                          # => (0/1)
```

The implicit conversions go further, adding to_str and to_ary so that the object splats and coerces like an empty array:

```ruby
require "naught"

NullObject = Naught.build do |config|
  config.define_implicit_conversions
end

null = NullObject.new

null.to_str                     # => ""
null.to_ary                     # => []

a, b, c = []
a                               # => nil
b                               # => nil
c                               # => nil
x, y, z = null
x                               # => nil
y                               # => nil
z                               # => nil
```

The destructuring example is the point of the implicit set. Assigning a null object to three variables yields nils, matching what an empty array produces, so a method expecting a collection will not break merely because the collaborator was absent.

Ruby has an unfortunate history of objects that answer to implicit conversion methods it does not own, and to_str and to_ary are two of them. The cost of this configuration is that the null object becomes a plausible string or array to any code that inspects those, which can convert a missing collaborator into a silently empty result further down the call chain.

## mimic copies an interface, impersonate also passes the type check

Sometimes the null object you need is not a catch all, but a stand in for one specific class, so that code calling a method the null does not implement fails loudly. The mimic configuration stubs out the methods of a given class:

```ruby
require "naught"

NullIO = Naught.build do |config|
  config.mimic IO
end

null_io = NullIO.new

null_io << "foo"                # => nil
null_io.readline                # => nil
null_io.foobar                  # =>
# ~> -:11:in `<main>': undefined method `foobar' for
#  <null:IO>:NullIO (NoMethodError)
```

That NoMethodError is the feature. A mimic object answers the methods the real class has and raises on everything else, so a typo in production code surfaces instead of being absorbed. It is the opposite trade-off to the plain Naught.build object.

The impersonate configuration goes one step further and derives the generated class from the impersonated class, which the documentation describes as handy when refitting legacy code that contains type checks:

```ruby
require "naught"

NullIO = Naught.build do |config|
  config.impersonate IO
end

null_io = NullIO.new
IO === null_io                  # => true

case null_io
when IO
  puts "Yep, checks out!"
  null_io << "some output"
else
  raise "Hey, I expected an IO!"
end
```

Deriving from the real class is a heavier technique than mimic, since it inherits whatever the class already defines, but it is the only way to satisfy code that uses === or a case statement to branch on the type. Use it where a type check is unavoidable, and use mimic where you only need the method surface.

## mimic example: handles objects with methods defined at runtime

The most interesting configuration in the library is mimic with an example object rather than a class. The documentation asks the awkward question directly, about objects that are unique and special snowflakes with new methods added at runtime, and answers it: as long as you can create an object to serve as an example, naught can copy the interface of that object, both the methods defined by its class and its singleton methods.

```ruby
require "naught"
require "logging"

log = Logging.logger["test"]
log.info

NullLog = Naught.build do |config|
  config.mimic example: log
end

null_log = NullLog.new
null_log.info                  # => nil
```

The library also handles objects whose methods do not exist as real methods at all. The documentation notes that when you use mimic example:, naught automatically discovers dynamically defined methods, which is what makes it work with libraries like Stripe that use method_missing to define methods based on API response data:

```ruby
require "naught"
require "stripe"

invoice = Stripe::Invoice.retrieve("inv_123")

NullInvoice = Naught.build do |config|
  config.mimic example: invoice
end

null_invoice = NullInvoice.new
null_invoice.period_end        # => nil
null_invoice.amount_due        # => nil
null_invoice.customer          # => nil
```

The discovery is heuristic, and the documentation says so. Naught finds dynamic methods by checking whether the example object responds to keys, attribute_names, or to_h. If you want to turn that behaviour off, you pass include_dynamic: false. The cost of the heuristic is that it depends on the example object exposing one of those three methods, so a library that defines its dynamic methods in some other way will not be discovered automatically.

## No GitHub releases, but a changelog, a Steepfile and yardstick

The repository is not archived, and the last push was on 2026-07-13. There are no GitHub releases, which is a small but real fact for anyone pinning a version: the source of truth for released versions is the gem on rubygems.org, and the history of what changed lives in CHANGELOG.md in the repository root.

The project is small and well instrumented, which the top level layout shows. There is naught.gemspec, a Rakefile, lib for the implementation, test for the suite, and sig for the type signatures, plus a Steepfile at the root. That Steepfile means the project type checks with Steep, and the workflows in the repository cover tests, lint, docs and typecheck separately, so a change has to pass all four. The .rspec file configures the spec runner, .rubocop.yml the style rules, and .yardstick.yml the documentation coverage check, which is a tool that measures whether the public methods are documented.

The licence is MIT, in LICENSE.txt, which for a code generation library is unremarkable but worth stating plainly, since the generated classes end up inside someone else's product rather than being a separate dependency at runtime.

The upgrade cost of adopting naught is low in one direction and easy to underestimate in the other. It is low because the gem generates plain Ruby classes with no runtime dependency on naught itself, so an application that uses it does not carry the library into production once the classes exist. It is easy to underestimate because the decision of which flavour of null to use, catch all, black hole, mimic or impersonate, is a design decision that spreads through a codebase, and changing it later means revisiting every call site that depends on the current behaviour.

## Conclusion

naught earns a place in a Ruby codebase that passes loggers, clients and collaborators into methods and keeps apologising for them with nil checks, and it is a good fit when you want the same interface as a real object but with no behaviour. It is the wrong tool when you need to know that a call failed, since a null object that swallows every message will also swallow your typos, and it is unnecessary when you only need one logger, since the README says plainly that you do not need a null object library. Before adopting it, read the CHANGELOG.md in the repository against the version you pull from rubygems.org, because the project publishes no GitHub releases, and decide up front whether the object you are nulling should raise on unexpected messages or answer nil to everything.

## FAQ

### How do I install and use the naught gem in Ruby?

The gem is published on rubygems.org as naught, so you add it to your Gemfile and require it with require "naught". You then call Naught.build to get a null object class and instantiate it, for example NullObject = Naught.build followed by NullObject.new.

### What is the difference between a naught null object and a black hole?

A plain null object answers nil to any message, so a chain of calls stops at the first result. A black hole, built with config.black_hole, answers itself, so null.foo.bar.baz keeps going and returns the black hole, and chained mutations such as null << "hello" << "world" keep working.

### What do the mimic and impersonate options do in naught?

With mimic, the generated class only stubs the methods of the class you name, so calling an undefined method raises a NoMethodError. With impersonate, the generated class is derived from that class, which makes it pass type checks such as IO === null_io and case statements that branch on the type.

### How does naught handle objects with methods defined at runtime?

With mimic example:, naught copies the interface of a live object, including its singleton methods, and automatically discovers dynamically defined methods. Discovery works by checking whether the example object responds to keys, attribute_names or to_h, and you can disable it by passing include_dynamic: false.

### Does naught have a downside for catching errors?

Yes, and it is inherent to the pattern. A null object that answers nil to everything also absorbs typos in method names, and a black hole never raises at all. Use mimic or impersonate when you want unexpected messages to fail loudly instead of returning nil.

## Sources

- [avdi/naught on GitHub](https://github.com/avdi/naught)
- [Issues](https://github.com/avdi/naught/issues)
- [License: MIT](https://github.com/avdi/naught/blob/master/LICENSE)
- [README](https://github.com/avdi/naught/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/avdi-naught
