# When to Use `errors.Is` and `errors.As` Instead of Direct Error Equality in Go

> Learn when to use Go's errors.Is and errors.As for error checking over direct equality. Understand error wrapping and sentinel errors for robust Go code.

- Repository: [JetBrains/go-modern-guidelines](https://github.com/jetbrains/go-modern-guidelines)
- Tags: best-practices
- Published: 2026-08-30

---

**Use `errors.Is` to check if an error chain contains a specific sentinel error and `errors.As` to extract a concrete error type from a wrapped chain; reserve direct equality (`==`) only for errors guaranteed never to be wrapped.**

Go’s error handling has evolved significantly since Go 1.13 introduced error wrapping with the `%w` verb. According to the JetBrains go-modern-guidelines repository, understanding when to use `errors.Is` and `errors.As` instead of direct error equality is essential for writing robust code that handles wrapped errors correctly across multiple API boundaries.

## Why Direct Equality Fails with Modern Error Chains

In Go versions prior to 1.13, direct comparison (`err == targetErr`) was the standard way to check errors. However, modern Go code frequently wraps errors using `fmt.Errorf("%w", err)` to add context while preserving the original error for inspection.

Once an error is wrapped, direct equality checks return `false` even when the underlying error matches. The wrapped error becomes a new object containing the original, making `==` unreliable for error inspection in production codebases where layers add contextual information.

## Using errors.Is for Sentinel Error Detection

Use **`errors.Is`** when you need to determine whether an error chain contains a specific sentinel error. This function unwraps the error chain recursively, respecting both the `%w` format verb and custom `Is(error) bool` implementations defined on error types.

This is the preferred method for checking standard library sentinels like `os.ErrNotExist` or custom package-level variables defined with `errors.New("...")`. In [`internal/cli/cli_test.go`](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/cli/cli_test.go) at line 154, the guidelines demonstrate this pattern:

```go
if !errors.Is(err, errWriteFailed) {
    // Handle the specific write failure
}

```

The function traverses the entire error chain until it finds a match or exhausts all wrapped layers, making it safe for errors passed through multiple functions or libraries.

## Using errors.As for Type Extraction

Use **`errors.As`** when you need to retrieve a specific error type from the chain to access its exported fields. While `errors.Is` checks for identity, `errors.As` performs type assertion across the wrapped chain, stopping at the first matching instance.

This is essential when error details influence program logic. For example, extracting an `*os.PathError` allows inspection of the `Op`, `Path`, and underlying `Err` fields that would be inaccessible through sentinel checking alone:

```go
var pathErr *os.PathError
if errors.As(wrappedErr, &pathErr) {
    fmt.Printf("operation %s failed on %s: %v\n", 
        pathErr.Op, pathErr.Path, pathErr.Err)
}

```

Unlike direct type assertions (`err.(*os.PathError)`), which succeed only on unwrapped errors, `errors.As` handles wrapped errors transparently.

## When Direct Equality Is Technically Safe

Reserve **`err == sentinel`** only for errors guaranteed never to be wrapped. This theoretically applies to sentinel errors defined as package-level variables that remain unmodified throughout their lifetime and are never passed through `fmt.Errorf` or similar wrapping functions.

However, the JetBrains go-modern-guidelines explicitly discourage this approach for most production code. The repository's [`internal/cli/cli.go`](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/cli/cli.go) at line 131 demonstrates the preferred pattern even for standard library sentinels like `flag.ErrHelp`, using `errors.Is` rather than direct comparison to maintain compatibility with potential future wrapping changes.

## Implementation in the JetBrains Guidelines

The JetBrains go-modern-guidelines enforce these patterns through the `modernize` analyzer, targeting Go versions up to 1.27. The repository consistently uses standard library helpers over raw equality checks to ensure compatibility with Go's evolving error wrapping semantics.

Key files demonstrating these patterns include:

- [`internal/cli/cli_test.go`](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/cli/cli_test.go) (line 154): Unit tests validating wrapped sentinel errors with `errors.Is`
- [`internal/cli/cli.go`](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/cli/cli.go) (line 131): Real-world CLI implementation handling `flag.ErrHelp` via `errors.Is`
- [`README.md`](https://github.com/JetBrains/go-modern-guidelines/blob/main/README.md): Documentation of error handling APIs including experimental features like `errors.AsType[T]`
- [`FEATURES.md`](https://github.com/JetBrains/go-modern-guidelines/blob/main/FEATURES.md): Comprehensive list of supported language features emphasizing error-handling utilities

By adhering to these patterns, code remains robust against changes in error wrapping semantics and aligns with modern Go best practices.

## Summary

- **Use `errors.Is`** to check if an error chain contains a specific sentinel error, as it recursively unwraps `%w` wrapped errors and respects custom `Is` methods.
- **Use `errors.As`** to extract concrete error types from a wrapped chain when you need to inspect error-specific fields like those in `*os.PathError`.
- **Avoid direct equality** (`==`) except for errors guaranteed never to be wrapped, which is rare in production codebases that follow modern error handling conventions.
- The JetBrains guidelines demonstrate these patterns in [`internal/cli/cli_test.go`](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/cli/cli_test.go) and [`internal/cli/cli.go`](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/cli/cli.go), favoring long-term compatibility over micro-optimizations.

## Frequently Asked Questions

### What happens if I use == instead of errors.Is on a wrapped error?

Direct equality checks return `false` because wrapping with `fmt.Errorf("%w", err)` creates a new error object that contains—but is not identical to—the original. The `==` operator compares object identity, not semantic equivalence, causing it to miss errors buried inside wrapped chains even when the underlying cause matches.

### Can errors.Is be used with custom error types or just sentinels?

Yes, `errors.Is` works with any error implementing the `Is(error) bool` method. While commonly used with `errors.New` sentinels, custom error types can define their own `Is` methods to implement domain-specific matching logic that persists across wrapping layers.

### How does errors.As differ from a standard type assertion?

A standard type assertion (`err.(*MyError)`) checks only the top-level error type and panics or fails if the error is wrapped. In contrast, `errors.As` recursively unwraps the error chain, attempting the type assertion at each level until it finds a match or exhausts the chain, making it safe for wrapped errors.

### When is it actually safe to use direct error equality in Go?

Direct equality is technically safe only for package-level sentinel variables that are guaranteed never to be wrapped by `fmt.Errorf` or passed through APIs that might add context. However, the JetBrains guidelines recommend treating all errors as potentially wrapped to future-proof code against refactoring and third-party API changes that might introduce wrapping.