about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAlan Pearce2025-03-12 22:39:51 +0100
committerAlan Pearce2025-03-12 22:39:51 +0100
commit9015baf955c94a806c01b3dcd5648c8e68ad2685 (patch)
tree5f59386c2ab31b6e45b85576e45a1fc8ae448ae0
parent7bb77ff5729cc9434afee895a470fd3b4c12e6d1 (diff)
downloadsearchix-9015baf955c94a806c01b3dcd5648c8e68ad2685.tar.lz
searchix-9015baf955c94a806c01b3dcd5648c8e68ad2685.tar.zst
searchix-9015baf955c94a806c01b3dcd5648c8e68ad2685.zip
refactor: ensure errors have stack traces HEAD main
-rw-r--r--frontend/assets.go10
-rw-r--r--go.mod3
-rw-r--r--go.sum2
-rw-r--r--gomod2nix.toml3
-rw-r--r--internal/components/markdown.templ4
-rw-r--r--internal/components/page.templ2
-rw-r--r--internal/config/config.go4
-rw-r--r--internal/config/fetcher.go3
-rw-r--r--internal/config/repository.go6
-rw-r--r--internal/fetcher/channel.go25
-rw-r--r--internal/fetcher/download.go25
-rw-r--r--internal/fetcher/http.go40
-rw-r--r--internal/fetcher/main.go6
-rw-r--r--internal/fetcher/nixpkgs-channel.go11
-rw-r--r--internal/file/utils.go14
-rw-r--r--internal/importer/importer.go7
-rw-r--r--internal/importer/main.go8
-rw-r--r--internal/importer/options.go8
-rw-r--r--internal/importer/package.go13
-rw-r--r--internal/importer/utils.go8
-rw-r--r--internal/index/index_meta.go18
-rw-r--r--internal/index/indexer.go59
-rw-r--r--internal/index/search.go12
-rw-r--r--internal/programs/programs.go13
-rw-r--r--internal/server/dev.go6
-rw-r--r--internal/server/logging.go2
-rw-r--r--internal/server/mux.go26
-rw-r--r--internal/server/server.go6
-rw-r--r--searchix.go12
29 files changed, 179 insertions, 177 deletions
diff --git a/frontend/assets.go b/frontend/assets.go
index b1c2efa..831ea84 100644
--- a/frontend/assets.go
+++ b/frontend/assets.go
@@ -8,7 +8,7 @@ import (
 	"io"
 	"io/fs"
 
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 )
 
 var Assets = &AssetCollection{
@@ -30,7 +30,7 @@ type AssetCollection struct {
 	ByPath      map[string]*Asset
 }
 
-func newAsset(filename string) (*Asset, error) {
+func newAsset(filename string) (*Asset, errors.E) {
 	file, err := Files.Open(filename)
 	if err != nil {
 		return nil, errors.WithMessagef(err, "could not open file %s", filename)
@@ -51,7 +51,7 @@ func newAsset(filename string) (*Asset, error) {
 	}, nil
 }
 
-func hashScripts() error {
+func hashScripts() errors.E {
 	scripts, err := fs.Glob(Files, "static/**.js")
 	if err != nil {
 		return errors.WithMessage(err, "could not glob files")
@@ -68,7 +68,7 @@ func hashScripts() error {
 	return nil
 }
 
-func hashStyles() error {
+func hashStyles() errors.E {
 	styles, err := fs.Glob(Files, "static/**.css")
 	if err != nil {
 		return errors.WithMessage(err, "could not glob files")
@@ -85,7 +85,7 @@ func hashStyles() error {
 	return nil
 }
 
-func Rehash() (err error) {
+func Rehash() (err errors.E) {
 	Assets.Scripts = []*Asset{}
 	err = hashScripts()
 	if err != nil {
diff --git a/go.mod b/go.mod
index 538a3f3..106a618 100644
--- a/go.mod
+++ b/go.mod
@@ -15,9 +15,9 @@ require (
 	github.com/mitchellh/mapstructure v1.5.0
 	github.com/osdevisnot/sorvor v0.4.4
 	github.com/pelletier/go-toml/v2 v2.2.3
-	github.com/pkg/errors v0.9.1
 	github.com/stoewer/go-strcase v1.3.0
 	github.com/yuin/goldmark v1.7.8
+	gitlab.com/tozd/go/errors v0.10.0
 	go.alanpearce.eu/x v0.0.0-20241203124832-a29434dba11a
 	go.uber.org/zap v1.27.0
 	golang.org/x/net v0.33.0
@@ -56,6 +56,7 @@ require (
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/mschoch/smat v0.2.0 // indirect
 	github.com/ncruces/go-strftime v0.1.9 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	github.com/stretchr/testify v1.10.0 // indirect
 	github.com/sykesm/zap-logfmt v0.0.4 // indirect
diff --git a/go.sum b/go.sum
index ce25867..60f4bba 100644
--- a/go.sum
+++ b/go.sum
@@ -140,6 +140,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
 github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+gitlab.com/tozd/go/errors v0.10.0 h1:A98kL+gaDvWnY6ZB/u8zP+sYaWsWUGBHeFMtamvW/74=
+gitlab.com/tozd/go/errors v0.10.0/go.mod h1:q3Ugr0C8dCzMEkrzjjlV2qNsm9e0KvqBjwcbcjCpBe4=
 go.alanpearce.eu/x v0.0.0-20241203124832-a29434dba11a h1:NUv3AzGxwMVSq26takww8/nyl+sPO2BsESoVSU8G49U=
 go.alanpearce.eu/x v0.0.0-20241203124832-a29434dba11a/go.mod h1:FRM6J9HMQ/RV2Q5j+6RKBYWh/YNeEUriGSqDRchiHuQ=
 go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
diff --git a/gomod2nix.toml b/gomod2nix.toml
index 6f342ac..0f6e067 100644
--- a/gomod2nix.toml
+++ b/gomod2nix.toml
@@ -151,6 +151,9 @@ schema = 3
   [mod."github.com/yuin/goldmark"]
     version = "v1.7.8"
     hash = "sha256-SNJMPPiXkRDLVOldrHN0ErC3bUB2VoWaLDkd9zmMATw="
+  [mod."gitlab.com/tozd/go/errors"]
+    version = "v0.10.0"
+    hash = "sha256-oW37KsieVKJOWk9ZXbGuQvuU4nyJCZzgYrTZHFkoCs4="
   [mod."go.alanpearce.eu/x"]
     version = "v0.0.0-20241203124832-a29434dba11a"
     hash = "sha256-ojqWkz3VqeAOevFxOTO5S3acRItCA4pUrTaul887+x8="
diff --git a/internal/components/markdown.templ b/internal/components/markdown.templ
index ff1212f..0ce98ad 100644
--- a/internal/components/markdown.templ
+++ b/internal/components/markdown.templ
@@ -5,9 +5,9 @@ import (
 
 	"go.alanpearce.eu/searchix/internal/nix"
 
+	"context"
 	"github.com/yuin/goldmark"
 	"github.com/yuin/goldmark/extension"
-	"context"
 	"io"
 )
 
@@ -27,7 +27,7 @@ func firstSentence[T ~string](text T) T {
 }
 
 func markdown(text nix.Markdown) templ.Component {
-	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
+	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) errors.E {
 		err := md.Convert([]byte(text), w)
 
 		return err
diff --git a/internal/components/page.templ b/internal/components/page.templ
index 4ba3741..63d0439 100644
--- a/internal/components/page.templ
+++ b/internal/components/page.templ
@@ -81,7 +81,7 @@ templ script(s *frontend.Asset) {
 }
 
 func Unsafe(html string) templ.Component {
-	return templ.ComponentFunc(func(_ context.Context, w io.Writer) (err error) {
+	return templ.ComponentFunc(func(_ context.Context, w io.Writer) (err errors.E) {
 		_, err = io.WriteString(w, html)
 		return
 	})
diff --git a/internal/config/config.go b/internal/config/config.go
index b2af53c..2822d0c 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -7,7 +7,7 @@ import (
 	"time"
 
 	"github.com/pelletier/go-toml/v2"
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 	"go.alanpearce.eu/x/log"
 )
 
@@ -103,7 +103,7 @@ func mustLocalTime(in string) (time LocalTime) {
 	return
 }
 
-func GetConfig(filename string, log *log.Logger) (*Config, error) {
+func GetConfig(filename string, log *log.Logger) (*Config, errors.E) {
 	config := DefaultConfig
 	if filename != "" {
 		log.Debug("reading config", "filename", filename)
diff --git a/internal/config/fetcher.go b/internal/config/fetcher.go
index fd95b32..fca6cb1 100644
--- a/internal/config/fetcher.go
+++ b/internal/config/fetcher.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	"github.com/stoewer/go-strcase"
+	"gitlab.com/tozd/go/errors"
 )
 
 type Fetcher int
@@ -37,7 +38,7 @@ func ParseFetcher(name string) (Fetcher, error) {
 	case "download":
 		return Download, nil
 	default:
-		return UnknownFetcher, fmt.Errorf("unsupported fetcher %s", name)
+		return UnknownFetcher, errors.Errorf("unsupported fetcher %s", name)
 	}
 }
 
diff --git a/internal/config/repository.go b/internal/config/repository.go
index a074cbc..52b255e 100644
--- a/internal/config/repository.go
+++ b/internal/config/repository.go
@@ -3,6 +3,8 @@ package config
 import (
 	"fmt"
 	"strings"
+
+	"gitlab.com/tozd/go/errors"
 )
 
 type RepoType int
@@ -28,12 +30,12 @@ func (f RepoType) String() string {
 	}
 }
 
-func parseRepoType(name string) (RepoType, error) {
+func parseRepoType(name string) (RepoType, errors.E) {
 	switch strings.ToLower(name) {
 	case "github":
 		return GitHub, nil
 	default:
-		return UnknownRepoType, fmt.Errorf("unsupported repo type %s", name)
+		return UnknownRepoType, errors.Errorf("unsupported repo type %s", name)
 	}
 }
 
diff --git a/internal/fetcher/channel.go b/internal/fetcher/channel.go
index 8f0aa03..86e2c60 100644
--- a/internal/fetcher/channel.go
+++ b/internal/fetcher/channel.go
@@ -14,7 +14,7 @@ import (
 	"go.alanpearce.eu/searchix/internal/index"
 	"go.alanpearce.eu/x/log"
 
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 )
 
 type ChannelFetcher struct {
@@ -26,7 +26,7 @@ type ChannelFetcher struct {
 func NewChannelFetcher(
 	source *config.Source,
 	logger *log.Logger,
-) (*ChannelFetcher, error) {
+) (*ChannelFetcher, errors.E) {
 	switch source.Importer {
 	case config.Options:
 		return &ChannelFetcher{
@@ -34,14 +34,14 @@ func NewChannelFetcher(
 			Logger: logger,
 		}, nil
 	default:
-		return nil, fmt.Errorf("unsupported importer type %s", source.Importer)
+		return nil, errors.Errorf("unsupported importer type %s", source.Importer)
 	}
 }
 
 func (i *ChannelFetcher) FetchIfNeeded(
 	ctx context.Context,
 	sourceMeta *index.SourceMeta,
-) (f FetchedFiles, err error) {
+) (*FetchedFiles, errors.E) {
 	args := []string{
 		"--no-build-output",
 		"--timeout",
@@ -58,12 +58,9 @@ func (i *ChannelFetcher) FetchIfNeeded(
 
 	i.Logger.Debug("nix-build command", "args", args)
 	cmd := exec.CommandContext(ctx, "nix-build", args...)
-	var out []byte
-	out, err = cmd.Output()
+	out, err := cmd.Output()
 	if err != nil {
-		err = errors.WithMessage(err, "failed to run nix-build (--dry-run)")
-
-		return
+		return nil, errors.WithMessage(err, "failed to run nix-build (--dry-run)")
 	}
 
 	outPath := path.Join(strings.TrimSpace(string(out)), i.Source.OutputPath, "options.json")
@@ -80,14 +77,10 @@ func (i *ChannelFetcher) FetchIfNeeded(
 
 	file, err := os.Open(outPath)
 	if err != nil {
-		err = errors.WithMessage(err, "failed to open options.json")
-
-		return
+		return nil, errors.WithMessage(err, "failed to open options.json")
 	}
 
-	f = FetchedFiles{
+	return &FetchedFiles{
 		Options: file,
-	}
-
-	return
+	}, nil
 }
diff --git a/internal/fetcher/download.go b/internal/fetcher/download.go
index a34c838..6c5bae8 100644
--- a/internal/fetcher/download.go
+++ b/internal/fetcher/download.go
@@ -2,13 +2,12 @@ package fetcher
 
 import (
 	"context"
-	"fmt"
 	"net/url"
 
 	"go.alanpearce.eu/searchix/internal/config"
 	"go.alanpearce.eu/searchix/internal/index"
 
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 	"go.alanpearce.eu/x/log"
 )
 
@@ -21,7 +20,7 @@ type DownloadFetcher struct {
 func NewDownloadFetcher(
 	source *config.Source,
 	logger *log.Logger,
-) (*DownloadFetcher, error) {
+) (*DownloadFetcher, errors.E) {
 	switch source.Importer {
 	case config.Options:
 		return &DownloadFetcher{
@@ -29,7 +28,7 @@ func NewDownloadFetcher(
 			Logger: logger,
 		}, nil
 	default:
-		return nil, fmt.Errorf("unsupported importer type %s", source.Importer)
+		return nil, errors.Errorf("unsupported importer type %s", source.Importer)
 	}
 }
 
@@ -41,20 +40,18 @@ var files = map[string]string{
 func (i *DownloadFetcher) FetchIfNeeded(
 	ctx context.Context,
 	sourceMeta *index.SourceMeta,
-) (f FetchedFiles, err error) {
-	var fetchURL string
+) (*FetchedFiles, errors.E) {
+	f := &FetchedFiles{}
 	sourceUpdated := sourceMeta.Updated
 	for key, filename := range files {
-		fetchURL, err = url.JoinPath(i.Source.URL, filename)
-		if err != nil {
-			err = errors.WithMessagef(
-				err,
+		fetchURL, baseErr := url.JoinPath(i.Source.URL, filename)
+		if baseErr != nil {
+			return nil, errors.WithMessagef(
+				baseErr,
 				"could not build URL with elements %s and %s",
 				i.Source.URL,
 				filename,
 			)
-
-			return
 		}
 
 		i.Logger.Debug("preparing to fetch URL", "url", fetchURL)
@@ -63,7 +60,7 @@ func (i *DownloadFetcher) FetchIfNeeded(
 		if err != nil {
 			i.Logger.Warn("failed to fetch file", "url", fetchURL, "error", err)
 
-			return f, err
+			return nil, err
 		}
 		// don't bother to issue requests for the later files
 		if mtime.Before(sourceUpdated) {
@@ -81,5 +78,5 @@ func (i *DownloadFetcher) FetchIfNeeded(
 		}
 	}
 
-	return
+	return f, nil
 }
diff --git a/internal/fetcher/http.go b/internal/fetcher/http.go
index c5ec8fc..ba99c3a 100644
--- a/internal/fetcher/http.go
+++ b/internal/fetcher/http.go
@@ -11,7 +11,7 @@ import (
 	"go.alanpearce.eu/searchix/internal/config"
 
 	"github.com/andybalholm/brotli"
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 	"go.alanpearce.eu/x/log"
 )
 
@@ -36,17 +36,20 @@ func fetchFileIfNeeded(
 	log *log.Logger,
 	mtime time.Time,
 	url string,
-) (body io.ReadCloser, newMtime time.Time, err error) {
+) (io.ReadCloser, time.Time, errors.E) {
+	var newMtime time.Time
 	var ifModifiedSince string
 	if !mtime.IsZero() {
 		ifModifiedSince = strings.Replace(mtime.UTC().Format(time.RFC1123), "UTC", "GMT", 1)
 	}
 
-	req, err := http.NewRequestWithContext(ctx, "GET", url, http.NoBody)
-	if err != nil {
-		err = errors.WithMessagef(err, "could not create HTTP request for %s", url)
-
-		return
+	req, baseErr := http.NewRequestWithContext(ctx, "GET", url, http.NoBody)
+	if baseErr != nil {
+		return nil, newMtime, errors.WithMessagef(
+			baseErr,
+			"could not create HTTP request for %s",
+			url,
+		)
 	}
 
 	req.Header.Set("User-Agent", fmt.Sprintf("Searchix %s", config.Version))
@@ -54,21 +57,22 @@ func fetchFileIfNeeded(
 	if ifModifiedSince != "" {
 		req.Header.Set("If-Modified-Since", ifModifiedSince)
 	}
-	res, err := http.DefaultClient.Do(req)
-	if err != nil {
-		err = errors.WithMessagef(err, "could not make HTTP request to %s", url)
-
-		return
+	res, baseErr := http.DefaultClient.Do(req)
+	if baseErr != nil {
+		return nil, newMtime, errors.WithMessagef(baseErr, "could not make HTTP request to %s", url)
 	}
 
+	var body io.ReadCloser
+	var err errors.E
 	switch res.StatusCode {
 	case http.StatusNotModified:
 		newMtime = mtime
 
-		return
+		return nil, newMtime, nil
 	case http.StatusOK:
-		newMtime, err = time.Parse(time.RFC1123, res.Header.Get("Last-Modified"))
-		if err != nil {
+		var baseErr error
+		newMtime, baseErr = time.Parse(time.RFC1123, res.Header.Get("Last-Modified"))
+		if baseErr != nil {
 			log.Warn(
 				"could not parse Last-Modified header from response",
 				"value",
@@ -84,11 +88,11 @@ func fetchFileIfNeeded(
 		case "", "identity", "gzip":
 			body = res.Body
 		default:
-			err = fmt.Errorf("cannot handle a body with content-encoding %s", ce)
+			err = errors.Errorf("cannot handle a body with content-encoding %s", ce)
 		}
 	default:
-		err = fmt.Errorf("got response code %d, don't know what to do", res.StatusCode)
+		err = errors.Errorf("got response code %d, don't know what to do", res.StatusCode)
 	}
 
-	return
+	return body, newMtime, err
 }
diff --git a/internal/fetcher/main.go b/internal/fetcher/main.go
index ac40ead..c027fbb 100644
--- a/internal/fetcher/main.go
+++ b/internal/fetcher/main.go
@@ -8,7 +8,7 @@ import (
 	"go.alanpearce.eu/searchix/internal/index"
 	"go.alanpearce.eu/x/log"
 
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 )
 
 type FetchedFiles struct {
@@ -18,13 +18,13 @@ type FetchedFiles struct {
 }
 
 type Fetcher interface {
-	FetchIfNeeded(context.Context, *index.SourceMeta) (FetchedFiles, error)
+	FetchIfNeeded(context.Context, *index.SourceMeta) (*FetchedFiles, errors.E)
 }
 
 func New(
 	source *config.Source,
 	logger *log.Logger,
-) (fetcher Fetcher, err error) {
+) (fetcher Fetcher, err errors.E) {
 	switch source.Fetcher {
 	case config.ChannelNixpkgs:
 		fetcher, err = NewNixpkgsChannelFetcher(source, logger)
diff --git a/internal/fetcher/nixpkgs-channel.go b/internal/fetcher/nixpkgs-channel.go
index 6f8ca63..0424602 100644
--- a/internal/fetcher/nixpkgs-channel.go
+++ b/internal/fetcher/nixpkgs-channel.go
@@ -2,13 +2,12 @@ package fetcher
 
 import (
 	"context"
-	"fmt"
 	"net/url"
 
 	"go.alanpearce.eu/searchix/internal/config"
 	"go.alanpearce.eu/searchix/internal/index"
 
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 	"go.alanpearce.eu/x/log"
 )
 
@@ -17,7 +16,7 @@ type NixpkgsChannelFetcher struct {
 	Logger *log.Logger
 }
 
-func makeChannelURL(channel string, subPath string) (string, error) {
+func makeChannelURL(channel string, subPath string) (string, errors.E) {
 	url, err := url.JoinPath("https://channels.nixos.org/", channel, subPath)
 
 	return url, errors.WithMessagef(err, "error creating URL")
@@ -26,7 +25,7 @@ func makeChannelURL(channel string, subPath string) (string, error) {
 func NewNixpkgsChannelFetcher(
 	source *config.Source,
 	logger *log.Logger,
-) (*NixpkgsChannelFetcher, error) {
+) (*NixpkgsChannelFetcher, errors.E) {
 	switch source.Importer {
 	case config.Options, config.Packages:
 		return &NixpkgsChannelFetcher{
@@ -34,7 +33,7 @@ func NewNixpkgsChannelFetcher(
 			Logger: logger,
 		}, nil
 	default:
-		return nil, fmt.Errorf("unsupported importer type %s", source.Importer)
+		return nil, errors.Errorf("unsupported importer type %s", source.Importer)
 	}
 }
 
@@ -47,7 +46,7 @@ const (
 func (i *NixpkgsChannelFetcher) FetchIfNeeded(
 	ctx context.Context,
 	sourceMeta *index.SourceMeta,
-) (f FetchedFiles, err error) {
+) (f *FetchedFiles, err errors.E) {
 
 	filesToFetch := make([]string, 2)
 	filesToFetch[0] = revisionFilename
diff --git a/internal/file/utils.go b/internal/file/utils.go
index efcf0f6..eb04fa0 100644
--- a/internal/file/utils.go
+++ b/internal/file/utils.go
@@ -5,10 +5,10 @@ import (
 	"io/fs"
 	"os"
 
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 )
 
-func Mkdirp(dir string) error {
+func Mkdirp(dir string) errors.E {
 	err := os.MkdirAll(dir, os.ModeDir|os.ModePerm)
 	if err != nil {
 		return errors.WithMessagef(err, "could not create directory %s", dir)
@@ -17,27 +17,27 @@ func Mkdirp(dir string) error {
 	return nil
 }
 
-func NeedNotExist(err error) error {
+func NeedNotExist(err error) errors.E {
 	if err != nil && !errors.Is(err, fs.ErrNotExist) {
-		return err
+		return errors.WithStack(err)
 	}
 
 	return nil
 }
 
-func StatIfExists(file string) (fs.FileInfo, error) {
+func StatIfExists(file string) (fs.FileInfo, errors.E) {
 	stat, err := os.Stat(file)
 
 	return stat, NeedNotExist(err)
 }
 
-func Exists(file string) (bool, error) {
+func Exists(file string) (bool, errors.E) {
 	stat, err := StatIfExists(file)
 
 	return stat != nil, err
 }
 
-func WriteToFile(path string, body io.Reader) error {
+func WriteToFile(path string, body io.Reader) errors.E {
 	file, err := os.Create(path)
 	if err != nil {
 		return errors.WithMessagef(err, "error creating file at %s", path)
diff --git a/internal/importer/importer.go b/internal/importer/importer.go
index 0b2db43..9334f7d 100644
--- a/internal/importer/importer.go
+++ b/internal/importer/importer.go
@@ -4,13 +4,14 @@ import (
 	"context"
 	"sync"
 
+	"gitlab.com/tozd/go/errors"
 	"go.alanpearce.eu/searchix/internal/index"
 	"go.alanpearce.eu/searchix/internal/nix"
 	"go.alanpearce.eu/x/log"
 )
 
 type Processor interface {
-	Process(context.Context) (<-chan nix.Importable, <-chan error)
+	Process(context.Context) (<-chan nix.Importable, <-chan errors.E)
 }
 
 func process(
@@ -18,7 +19,7 @@ func process(
 	indexer *index.WriteIndex,
 	processor Processor,
 	logger *log.Logger,
-) (bool, error) {
+) (bool, errors.E) {
 	wg := sync.WaitGroup{}
 
 	wg.Add(1)
@@ -28,7 +29,7 @@ func process(
 	iErrs := indexer.Import(ctx, objects)
 
 	var hadObjectErrors bool
-	var criticalError error
+	var criticalError errors.E
 	go func() {
 		for {
 			select {
diff --git a/internal/importer/main.go b/internal/importer/main.go
index 7181926..e2c222c 100644
--- a/internal/importer/main.go
+++ b/internal/importer/main.go
@@ -14,7 +14,7 @@ import (
 	"go.alanpearce.eu/searchix/internal/programs"
 	"go.alanpearce.eu/x/log"
 
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 )
 
 func createSourceImporter(
@@ -23,8 +23,8 @@ func createSourceImporter(
 	meta *index.Meta,
 	indexer *index.WriteIndex,
 	forceUpdate bool,
-) func(*config.Source) error {
-	return func(source *config.Source) error {
+) func(*config.Source) errors.E {
+	return func(source *config.Source) errors.E {
 		logger := log.With(
 			"name",
 			source.Key,
@@ -165,7 +165,7 @@ func (imp *Importer) Start(
 	ctx context.Context,
 	forceUpdate bool,
 	onlyUpdateSources *[]string,
-) error {
+) errors.E {
 	if len(imp.config.Importer.Sources) == 0 {
 		imp.log.Info("No sources enabled")
 
diff --git a/internal/importer/options.go b/internal/importer/options.go
index 763f57f..a586a3f 100644
--- a/internal/importer/options.go
+++ b/internal/importer/options.go
@@ -11,7 +11,7 @@ import (
 
 	"github.com/bcicen/jstream"
 	"github.com/mitchellh/mapstructure"
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 )
 
 type nixValueJSON struct {
@@ -68,7 +68,7 @@ func NewOptionProcessor(
 	infile io.ReadCloser,
 	source *config.Source,
 	log *log.Logger,
-) (*OptionIngester, error) {
+) (*OptionIngester, errors.E) {
 	i := OptionIngester{
 		dec:     jstream.NewDecoder(infile, 1).EmitKV(),
 		log:     log,
@@ -94,9 +94,9 @@ func NewOptionProcessor(
 	return &i, nil
 }
 
-func (i *OptionIngester) Process(ctx context.Context) (<-chan nix.Importable, <-chan error) {
+func (i *OptionIngester) Process(ctx context.Context) (<-chan nix.Importable, <-chan errors.E) {
 	results := make(chan nix.Importable)
-	errs := make(chan error)
+	errs := make(chan errors.E)
 
 	go func() {
 		defer i.infile.Close()
diff --git a/internal/importer/package.go b/internal/importer/package.go
index 59bccd8..34293a7 100644
--- a/internal/importer/package.go
+++ b/internal/importer/package.go
@@ -14,7 +14,7 @@ import (
 
 	"github.com/bcicen/jstream"
 	"github.com/mitchellh/mapstructure"
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 )
 
 type packageJSON struct {
@@ -69,7 +69,7 @@ func NewPackageProcessor(
 	source *config.Source,
 	log *log.Logger,
 	programsDB *programs.DB,
-) (*PackageIngester, error) {
+) (*PackageIngester, errors.E) {
 	i := &PackageIngester{
 		dec:      jstream.NewDecoder(infile, 2).EmitKV(),
 		log:      log,
@@ -116,9 +116,9 @@ func convertToLicense(in map[string]any) *nix.License {
 	return l
 }
 
-func (i *PackageIngester) Process(ctx context.Context) (<-chan nix.Importable, <-chan error) {
+func (i *PackageIngester) Process(ctx context.Context) (<-chan nix.Importable, <-chan errors.E) {
 	results := make(chan nix.Importable)
-	errs := make(chan error)
+	errs := make(chan errors.E)
 
 	if i.programs != nil {
 		err := i.programs.Open()
@@ -135,7 +135,7 @@ func (i *PackageIngester) Process(ctx context.Context) (<-chan nix.Importable, <
 
 	outer:
 		for mv := range i.dec.Stream() {
-			var err error
+			var err errors.E
 			var programs []string
 			select {
 			case <-ctx.Done():
@@ -222,8 +222,7 @@ func (i *PackageIngester) Process(ctx context.Context) (<-chan nix.Importable, <
 			}
 
 			i.pkg = packageJSON{}
-			err = i.ms.Decode(x) // stores in i.pkg
-			if err != nil {
+			if err := i.ms.Decode(x); err != nil { // stores in i.pkg
 				errs <- errors.WithMessagef(err, "failed to decode package %#v", x)
 
 				continue
diff --git a/internal/importer/utils.go b/internal/importer/utils.go
index da10735..0ca6890 100644
--- a/internal/importer/utils.go
+++ b/internal/importer/utils.go
@@ -10,7 +10,7 @@ import (
 	"go.alanpearce.eu/searchix/internal/nix"
 
 	"github.com/bcicen/jstream"
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 )
 
 func ValueTypeToString(valueType jstream.ValueType) string {
@@ -34,7 +34,7 @@ func ValueTypeToString(valueType jstream.ValueType) string {
 	return "very strange"
 }
 
-func makeRepoURL(repo config.Repository, subPath string, line string) (string, error) {
+func makeRepoURL(repo config.Repository, subPath string, line string) (string, errors.E) {
 	switch repo.Type {
 	case config.GitHub:
 		ref := repo.Revision
@@ -55,7 +55,7 @@ func makeRepoURL(repo config.Repository, subPath string, line string) (string, e
 	}
 }
 
-func MakeChannelLink(repo config.Repository, subPath string) (*nix.Link, error) {
+func MakeChannelLink(repo config.Repository, subPath string) (*nix.Link, errors.E) {
 	url, err := makeRepoURL(repo, subPath, "")
 	if err != nil {
 		return nil, err
@@ -67,7 +67,7 @@ func MakeChannelLink(repo config.Repository, subPath string) (*nix.Link, error)
 	}, nil
 }
 
-func setRepoRevision(file io.ReadCloser, source *config.Source) error {
+func setRepoRevision(file io.ReadCloser, source *config.Source) errors.E {
 	if file != nil {
 		defer file.Close()
 		var str strings.Builder
diff --git a/internal/index/index_meta.go b/internal/index/index_meta.go
index 336bae0..635965a 100644
--- a/internal/index/index_meta.go
+++ b/internal/index/index_meta.go
@@ -8,7 +8,7 @@ import (
 	"go.alanpearce.eu/searchix/internal/file"
 	"go.alanpearce.eu/x/log"
 
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 )
 
 const CurrentSchemaVersion = 3
@@ -31,7 +31,7 @@ type Meta struct {
 	data
 }
 
-func createMeta(path string, log *log.Logger) (*Meta, error) {
+func createMeta(path string, log *log.Logger) (*Meta, errors.E) {
 	exists, err := file.Exists(path)
 	if err != nil {
 		return nil, errors.WithMessage(err, "could not check for existence of index metadata")
@@ -49,7 +49,7 @@ func createMeta(path string, log *log.Logger) (*Meta, error) {
 	}, nil
 }
 
-func openMeta(path string, log *log.Logger) (*Meta, error) {
+func openMeta(path string, log *log.Logger) (*Meta, errors.E) {
 	exists, err := file.Exists(path)
 	if err != nil {
 		return nil, errors.WithMessage(err, "could not check for existence of index metadata")
@@ -58,16 +58,16 @@ func openMeta(path string, log *log.Logger) (*Meta, error) {
 		return createMeta(path, log)
 	}
 
-	j, err := os.ReadFile(path)
-	if err != nil {
-		return nil, errors.WithMessage(err, "could not open index metadata file")
+	j, baseErr := os.ReadFile(path)
+	if baseErr != nil {
+		return nil, errors.WithMessage(baseErr, "could not open index metadata file")
 	}
 	meta := Meta{
 		path: path,
 		log:  log,
 	}
-	err = json.Unmarshal(j, &meta.data)
-	if err != nil {
+
+	if err := json.Unmarshal(j, &meta.data); err != nil {
 		return nil, errors.WithMessage(err, "index metadata is corrupt, try replacing the index")
 	}
 
@@ -88,7 +88,7 @@ func (i *Meta) checkSchemaVersion() {
 	}
 }
 
-func (i *Meta) Save() error {
+func (i *Meta) Save() errors.E {
 	i.SchemaVersion = CurrentSchemaVersion
 	j, err := json.Marshal(i.data)
 	if err != nil {
diff --git a/internal/index/indexer.go b/internal/index/indexer.go
index 6a1dcf0..6000358 100644
--- a/internal/index/indexer.go
+++ b/internal/index/indexer.go
@@ -26,7 +26,7 @@ import (
 	"github.com/blevesearch/bleve/v2/document"
 	"github.com/blevesearch/bleve/v2/mapping"
 	indexAPI "github.com/blevesearch/bleve_index_api"
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 )
 
 type WriteIndex struct {
@@ -36,16 +36,16 @@ type WriteIndex struct {
 }
 
 type BatchError struct {
-	error
+	errors.E
 }
 
 func (e *BatchError) Error() string {
-	return e.error.Error()
+	return e.E.Error()
 }
 
 var batchSize = 10_000
 
-func createIndexMapping() (mapping.IndexMapping, error) {
+func createIndexMapping() (mapping.IndexMapping, errors.E) {
 	indexMapping := bleve.NewIndexMapping()
 	indexMapping.StoreDynamic = false
 	indexMapping.IndexDynamic = false
@@ -124,7 +124,7 @@ func createIndexMapping() (mapping.IndexMapping, error) {
 	return indexMapping, nil
 }
 
-func createIndex(indexPath string, options *Options) (bleve.Index, error) {
+func createIndex(indexPath string, options *Options) (bleve.Index, errors.E) {
 	indexMapping, err := createIndexMapping()
 	if err != nil {
 		return nil, err
@@ -136,15 +136,15 @@ func createIndex(indexPath string, options *Options) (bleve.Index, error) {
 			"PersisterNapUnderNumFiles": 500,
 		}
 	}
-	idx, err := bleve.NewUsing(
+	idx, baseErr := bleve.NewUsing(
 		indexPath,
 		indexMapping,
 		bleve.Config.DefaultIndexType,
 		bleve.Config.DefaultKVStore,
 		kvconfig,
 	)
-	if err != nil {
-		return nil, errors.WithMessagef(err, "unable to create index at path %s", indexPath)
+	if baseErr != nil {
+		return nil, errors.WithMessagef(baseErr, "unable to create index at path %s", indexPath)
 	}
 
 	return idx, nil
@@ -162,7 +162,7 @@ var expectedDataFiles = []string{
 	"nixpkgs-programs.db",
 }
 
-func deleteIndex(dataRoot string) error {
+func deleteIndex(dataRoot string) errors.E {
 	dir, err := os.ReadDir(dataRoot)
 	if err != nil {
 		return errors.WithMessagef(err, "could not read data directory %s", dataRoot)
@@ -195,8 +195,8 @@ func OpenOrCreate(
 	dataRoot string,
 	force bool,
 	options *Options,
-) (*ReadIndex, *WriteIndex, bool, error) {
-	var err error
+) (*ReadIndex, *WriteIndex, bool, errors.E) {
+	var err errors.E
 	bleve.SetLog(zap.NewStdLog(options.Logger.Named("bleve").GetLogger()))
 
 	indexPath := path.Join(dataRoot, indexBaseName)
@@ -231,9 +231,10 @@ func OpenOrCreate(
 		}
 
 	} else {
-		idx, err = bleve.Open(indexPath)
-		if err != nil {
-			return nil, nil, exists, errors.WithMessagef(err, "could not open index at path %s", indexPath)
+		var baseErr error
+		idx, baseErr = bleve.Open(indexPath)
+		if baseErr != nil {
+			return nil, nil, exists, errors.WithMessagef(baseErr, "could not open index at path %s", indexPath)
 		}
 
 		meta, err = openMeta(metaPath, options.Logger)
@@ -260,16 +261,16 @@ func OpenOrCreate(
 		nil
 }
 
-func (i *WriteIndex) SaveMeta() error {
+func (i *WriteIndex) SaveMeta() errors.E {
 	return i.Meta.Save()
 }
 
 func (i *WriteIndex) Import(
 	ctx context.Context,
 	objects <-chan nix.Importable,
-) <-chan error {
-	var err error
-	errs := make(chan error)
+) <-chan errors.E {
+	var err errors.E
+	errs := make(chan errors.E)
 
 	go func() {
 		defer close(errs)
@@ -288,8 +289,7 @@ func (i *WriteIndex) Import(
 			}
 
 			doc := document.NewDocument(nix.GetKey(obj))
-			err = indexMapping.MapDocument(doc, obj)
-			if err != nil {
+			if err := indexMapping.MapDocument(doc, obj); err != nil {
 				errs <- errors.WithMessagef(err, "could not map document for object: %s", obj.GetName())
 
 				continue
@@ -297,8 +297,7 @@ func (i *WriteIndex) Import(
 
 			var data bytes.Buffer
 			enc := gob.NewEncoder(&data)
-			err = enc.Encode(&obj)
-			if err != nil {
+			if err := enc.Encode(&obj); err != nil {
 				errs <- errors.WithMessage(err, "could not store object in search index")
 
 				continue
@@ -307,9 +306,7 @@ func (i *WriteIndex) Import(
 			newDoc := doc.AddField(field)
 
 			// log.Debug("adding object to index", "name", opt.Name)
-			err = batch.IndexAdvanced(newDoc)
-
-			if err != nil {
+			if err := batch.IndexAdvanced(newDoc); err != nil {
 				errs <- errors.WithMessagef(err, "could not index object %s", obj.GetName())
 
 				continue
@@ -334,11 +331,11 @@ func (i *WriteIndex) Import(
 	return errs
 }
 
-func (i *WriteIndex) Flush(batch *bleve.Batch) error {
+func (i *WriteIndex) Flush(batch *bleve.Batch) errors.E {
 	size := batch.Size()
 	if size == 0 {
 		return &BatchError{
-			error: errors.New("no documents to flush"),
+			E: errors.New("no documents to flush"),
 		}
 	}
 	i.log.Debug("flushing batch", "size", size)
@@ -346,7 +343,7 @@ func (i *WriteIndex) Flush(batch *bleve.Batch) error {
 	err := i.index.Batch(batch)
 	if err != nil {
 		return &BatchError{
-			error: errors.WithMessagef(err, "could not flush batch"),
+			E: errors.WithMessagef(err, "could not flush batch"),
 		}
 	}
 
@@ -355,7 +352,7 @@ func (i *WriteIndex) Flush(batch *bleve.Batch) error {
 	return nil
 }
 
-func (i *WriteIndex) Close() (err error) {
+func (i *WriteIndex) Close() (err errors.E) {
 	if e := i.Meta.Save(); e != nil {
 		// index needs to be closed anyway
 		err = errors.WithMessage(e, "could not save metadata")
@@ -368,7 +365,7 @@ func (i *WriteIndex) Close() (err error) {
 	return err
 }
 
-func (i *WriteIndex) DeleteBySource(source string) error {
+func (i *WriteIndex) DeleteBySource(source string) errors.E {
 	query := bleve.NewTermQuery(source)
 	search := bleve.NewSearchRequest(query)
 	search.Size = math.MaxInt
@@ -392,7 +389,7 @@ func (i *WriteIndex) DeleteBySource(source string) error {
 	}
 	err = i.Flush(batch)
 	if err != nil {
-		return err
+		return errors.WithStack(err)
 	}
 
 	if uint64(search.Size) < results.Total {
diff --git a/internal/index/search.go b/internal/index/search.go
index 50a5ce2..3f9c13a 100644
--- a/internal/index/search.go
+++ b/internal/index/search.go
@@ -12,7 +12,7 @@ import (
 	"github.com/blevesearch/bleve/v2"
 	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/query"
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 )
 
 const DefaultPageSize = 100
@@ -33,7 +33,7 @@ type ReadIndex struct {
 	meta  *Meta
 }
 
-func (index *ReadIndex) GetEnabledSources() ([]string, error) {
+func (index *ReadIndex) GetEnabledSources() ([]string, errors.E) {
 	facet := bleve.NewFacetRequest("Source", 100)
 	query := bleve.NewMatchAllQuery()
 	search := bleve.NewSearchRequest(query)
@@ -64,13 +64,13 @@ func setField[T query.FieldableQuery](
 func (index *ReadIndex) search(
 	ctx context.Context,
 	request *bleve.SearchRequest,
-) (*Result, error) {
+) (*Result, errors.E) {
 	request.Fields = []string{"_data", "Source"}
 
 	bleveResult, err := index.index.SearchInContext(ctx, request)
 	select {
 	case <-ctx.Done():
-		return nil, ctx.Err()
+		return nil, errors.WithStack(ctx.Err())
 	default:
 		if err != nil {
 			return nil, errors.WithMessage(err, "failed to execute search query")
@@ -104,7 +104,7 @@ func (index *ReadIndex) Search(
 	keyword string,
 	from int,
 	pageSize int,
-) (*Result, error) {
+) (*Result, errors.E) {
 	query := bleve.NewBooleanQuery()
 
 	// match the user's query in any field ...
@@ -157,7 +157,7 @@ func (index *ReadIndex) GetDocument(
 	ctx context.Context,
 	source *config.Source,
 	id string,
-) (*nix.Importable, error) {
+) (*nix.Importable, errors.E) {
 	key := nix.MakeKey(source, id)
 	query := bleve.NewDocIDQuery([]string{key})
 	search := bleve.NewSearchRequest(query)
diff --git a/internal/programs/programs.go b/internal/programs/programs.go
index 5088444..090bcf1 100644
--- a/internal/programs/programs.go
+++ b/internal/programs/programs.go
@@ -7,7 +7,7 @@ import (
 	"os/exec"
 	"strings"
 
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 	"go.alanpearce.eu/searchix/internal/config"
 	"go.alanpearce.eu/x/log"
 	_ "modernc.org/sqlite" //nolint:blank-imports // sqlite driver needed for database/sql
@@ -26,7 +26,7 @@ func Instantiate(
 	ctx context.Context,
 	source *config.Source,
 	logger *log.Logger,
-) (*DB, error) {
+) (*DB, errors.E) {
 	// nix-instantiate --eval --json -I nixpkgs=channel:nixos-unstable --expr 'toString <nixpkgs/programs.sqlite>'
 	args := []string{
 		"--eval",
@@ -53,7 +53,7 @@ func Instantiate(
 	}, nil
 }
 
-func (p *DB) Open() error {
+func (p *DB) Open() errors.E {
 	var err error
 	p.db, err = sql.Open("sqlite", p.Path)
 	if err != nil {
@@ -96,7 +96,7 @@ WHERE package = ?
 	return nil
 }
 
-func (p *DB) Close() error {
+func (p *DB) Close() errors.E {
 	if err := p.db.Close(); err != nil {
 		return errors.WithMessage(err, "failed to close sqlite database")
 	}
@@ -104,7 +104,8 @@ func (p *DB) Close() error {
 	return nil
 }
 
-func (p *DB) GetPackagePrograms(ctx context.Context, pkg string) (programs []string, err error) {
+func (p *DB) GetPackagePrograms(ctx context.Context, pkg string) ([]string, errors.E) {
+	programs := make([]string, 10)
 	if p.db == nil {
 		return nil, errors.New("database not open")
 	}
@@ -127,5 +128,5 @@ func (p *DB) GetPackagePrograms(ctx context.Context, pkg string) (programs []str
 		return nil, errors.WithMessage(rerr, "sql error")
 	}
 
-	return
+	return programs, nil
 }
diff --git a/internal/server/dev.go b/internal/server/dev.go
index f5fd4fd..89ac069 100644
--- a/internal/server/dev.go
+++ b/internal/server/dev.go
@@ -8,7 +8,7 @@ import (
 	"time"
 
 	"github.com/fsnotify/fsnotify"
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 	"go.alanpearce.eu/x/log"
 )
 
@@ -17,7 +17,7 @@ type FileWatcher struct {
 	log     *log.Logger
 }
 
-func NewFileWatcher(log *log.Logger) (*FileWatcher, error) {
+func NewFileWatcher(log *log.Logger) (*FileWatcher, errors.E) {
 	watcher, err := fsnotify.NewWatcher()
 	if err != nil {
 		return nil, errors.WithMessage(err, "could not create watcher")
@@ -29,7 +29,7 @@ func NewFileWatcher(log *log.Logger) (*FileWatcher, error) {
 	}, nil
 }
 
-func (i FileWatcher) AddRecursive(from string) error {
+func (i FileWatcher) AddRecursive(from string) errors.E {
 	i.log.Debug(fmt.Sprintf("watching files under %s", from))
 	err := filepath.WalkDir(from, func(path string, entry fs.DirEntry, err error) error {
 		if err != nil {
diff --git a/internal/server/logging.go b/internal/server/logging.go
index 5930b9d..e11b16f 100644
--- a/internal/server/logging.go
+++ b/internal/server/logging.go
@@ -3,7 +3,7 @@ package server
 import (
 	"net/http"
 
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 	"go.alanpearce.eu/x/log"
 )
 
diff --git a/internal/server/mux.go b/internal/server/mux.go
index 37435d4..1507860 100644
--- a/internal/server/mux.go
+++ b/internal/server/mux.go
@@ -25,7 +25,7 @@ import (
 	"go.alanpearce.eu/x/log"
 
 	"github.com/osdevisnot/sorvor/pkg/livereload"
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 )
 
 type HTTPError struct {
@@ -65,7 +65,7 @@ func NewMux(
 	index *search.ReadIndex,
 	log *log.Logger,
 	liveReload bool,
-) (*http.ServeMux, error) {
+) (*http.ServeMux, errors.E) {
 	if cfg == nil {
 		return nil, errors.New("cfg is nil")
 	}
@@ -171,15 +171,16 @@ func NewMux(
 
 				w.Header().Add("Cache-Control", "max-age=300")
 				w.Header().Add("Vary", "Fetch")
+				var baseErr error
 				if r.Header.Get("Fetch") == "true" {
 					w.Header().Add("Content-Type", "text/html; charset=utf-8")
-					err = components.Results(tdata).Render(r.Context(), w)
+					baseErr = components.Results(tdata).Render(r.Context(), w)
 				} else {
-					err = components.ResultsPage(tdata).Render(r.Context(), w)
+					baseErr = components.ResultsPage(tdata).Render(r.Context(), w)
 				}
-				if err != nil {
-					log.Error("template error", "template", importerType, "error", err)
-					errorHandler(w, r, err.Error(), http.StatusInternalServerError)
+				if baseErr != nil {
+					log.Error("template error", "template", importerType, "error", baseErr)
+					errorHandler(w, r, baseErr.Error(), http.StatusInternalServerError)
 				}
 			} else {
 				w.Header().Add("Cache-Control", "max-age=14400")
@@ -244,15 +245,16 @@ func NewMux(
 				Sources:       sources,
 				Assets:        frontend.Assets,
 			}
+			var baseErr error
 			if r.Header.Get("Fetch") == "true" {
 				w.Header().Add("Content-Type", "text/html; charset=utf-8")
-				err = components.Detail(*doc).Render(r.Context(), w)
+				baseErr = components.Detail(*doc).Render(r.Context(), w)
 			} else {
-				err = components.DetailPage(tdata, *doc).Render(r.Context(), w)
+				baseErr = components.DetailPage(tdata, *doc).Render(r.Context(), w)
 			}
-			if err != nil {
-				log.Error("template error", "template", importerSingular, "error", err)
-				errorHandler(w, r, err.Error(), http.StatusInternalServerError)
+			if baseErr != nil {
+				log.Error("template error", "template", importerSingular, "error", baseErr)
+				errorHandler(w, r, baseErr.Error(), http.StatusInternalServerError)
 			}
 		}
 	}
diff --git a/internal/server/server.go b/internal/server/server.go
index aacef30..17d3512 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -11,7 +11,7 @@ import (
 	"go.alanpearce.eu/searchix/internal/index"
 	"go.alanpearce.eu/x/log"
 
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 	"golang.org/x/net/http2"
 	"golang.org/x/net/http2/h2c"
 )
@@ -28,7 +28,7 @@ func New(
 	index *index.ReadIndex,
 	log *log.Logger,
 	liveReload bool,
-) (*Server, error) {
+) (*Server, errors.E) {
 	mux, err := NewMux(conf, index, log, liveReload)
 	if err != nil {
 		return nil, err
@@ -49,7 +49,7 @@ func New(
 	}, nil
 }
 
-func (s *Server) Start() error {
+func (s *Server) Start() errors.E {
 	listenAddress := net.JoinHostPort(s.cfg.Web.ListenAddress, strconv.Itoa(s.cfg.Web.Port))
 	l, err := net.Listen("tcp", listenAddress)
 	if err != nil {
diff --git a/searchix.go b/searchix.go
index be76af6..1c870df 100644
--- a/searchix.go
+++ b/searchix.go
@@ -14,7 +14,7 @@ import (
 	"go.alanpearce.eu/x/log"
 
 	"github.com/getsentry/sentry-go"
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 )
 
 func nextUTCOccurrenceOfTime(t config.LocalTime) time.Time {
@@ -44,7 +44,7 @@ type IndexOptions struct {
 	Logger    *log.Logger
 }
 
-func (s *Server) SetupIndex(ctx context.Context, options *IndexOptions) error {
+func (s *Server) SetupIndex(ctx context.Context, options *IndexOptions) errors.E {
 	var i uint
 	cfgEnabledSources := make([]string, len(s.cfg.Importer.Sources))
 	for key := range s.cfg.Importer.Sources {
@@ -139,7 +139,7 @@ type Server struct {
 	writeIndex *index.WriteIndex
 }
 
-func New(cfg *config.Config, log *log.Logger) (*Server, error) {
+func New(cfg *config.Config, log *log.Logger) (*Server, errors.E) {
 	err := sentry.Init(sentry.ClientOptions{
 		EnableTracing:    true,
 		TracesSampleRate: 1.0,
@@ -164,7 +164,7 @@ func (s *Server) startUpdateTimer(
 ) {
 	const monitorSlug = "import"
 	localHub.WithScope(func(scope *sentry.Scope) {
-		var err error
+		var err errors.E
 		scope.SetContext("monitor", sentry.Context{"slug": monitorSlug})
 		monitorConfig := &sentry.MonitorConfig{
 			Schedule:      sentry.IntervalSchedule(1, sentry.MonitorScheduleUnitDay),
@@ -219,8 +219,8 @@ func (s *Server) startUpdateTimer(
 	})
 }
 
-func (s *Server) Start(ctx context.Context, liveReload bool) error {
-	var err error
+func (s *Server) Start(ctx context.Context, liveReload bool) errors.E {
+	var err errors.E
 	s.sv, err = server.New(s.cfg, s.readIndex, s.log.Named("server"), liveReload)
 	if err != nil {
 		return errors.Wrap(err, "error setting up server")