Skip to content

Repository files navigation

ctxerrors

Go ReferenceCIcoverageversionlicenseimported by

 #### ##### # # ###### ##### ##### #### ##### #### # # # # # # # # # # # # # # # # # ## ##### # # # # # # # # #### # # ## # ##### ##### # # ##### # # # # # # # # # # # # # # # # # #### # # # ###### # # # # #### # # #### 

fuck yeah, another Go error handling package 🖕

A Go library that wraps errors with context information (file, line, function) because debugging without context is like trying to find your dick in the dark.

Table of Contents

Installation

go get github.com/psyb0t/ctxerrors

What the fuck does it do?

This package automatically captures where your errors happen in your code. No more hunting through logs like some dickless detective wondering where the fuck that error came from.

Functions

  • New() - Creates a new error with location context
  • Wrap() - Wraps existing errors with additional context and location
  • Wrapf() - Like Wrap() but with printf-style formatting because we're not animals
  • Join() - Squashes a pile of errors into one that still knows where they got squashed. For when one thing fans out and several bits can shit the bed independently — three log sinks, a batch of rows, whatever. See Joining errors.
  • SetErrorMap() / MapError() / ClearErrorMap() - Translate foreign sentinel errors (gorm, sql, redis...) into your own business errors at wrap time. See Error mapping.

New(), Wrap(), Wrapf(), and Join() return a *CTXError that implements the standard error interface and supports errors.Unwrap(), errors.Is(), and errors.As() because Go's error handling conventions aren't completely ass-backwards. SetErrorMap(), MapError(), and ClearErrorMap() don't return anything — they just manage the translation map.

Usage

Creating new errors

import"github.com/psyb0t/ctxerrors"funcdoSomething() error {
returnctxerrors.New("shit went sideways")
}

Wrapping existing errors

funcprocessFile(filenamestring) error {
file, err:=os.Open(filename)
iferr!=nil {
returnctxerrors.Wrap(err, "failed to open file")
}
deferfile.Close()
// do stuff...returnnil
}

Formatted wrapping

funcconnectToDatabase(hoststring, portint) error {
conn, err:=sql.Open("postgres", fmt.Sprintf("host=%s port=%d", host, port))
iferr!=nil {
returnctxerrors.Wrapf(err, "failed to connect to database at %s:%d", host, port)
}
deferconn.Close()
returnnil
}

Joining errors

Sometimes one operation fans out and more than one branch can fail on its own. Bailing on the first failure means the rest never even get attempted, and you find out about exactly one problem when there were three. Do all the work, collect what broke, hand it back as one error:

funcwriteToAllSinks(sinks []Sink, recordRecord) error {
varerrs []errorfori, sink:=rangesinks {
iferr:=sink.Write(record); err!=nil {
errs=append(errs, ctxerrors.Wrapf(err, "sink %d failed", i))
}
}
returnctxerrors.Join(errs...)
}

Join() ignores nil errors and returns nil when every one of them is nil, so you can hand it the slice without checking whether anything actually went wrong. The result unwraps to what the standard library's errors.Join produces, which means errors.Is() and errors.As() still find any of the individual errors you put in — and unlike the standard library version, the result knows the line where you joined them.

Error output

When shit hits the fan, you get detailed context:

failed to connect to database at localhost:5432: dial tcp [::1]:5432: connect: connection refused [/path/to/your/file.go:42 in main.connectToDatabase]

Error chaining

When you wrap ctxerrors in a chain, each layer shows its context:

funcreadConfig() error {
returnctxerrors.New("config file missing")
}
funcinitDatabase() error {
iferr:=readConfig(); err!=nil {
returnctxerrors.Wrap(err, "failed to read config")
}
returnnil
}
funcstartServer() error {
iferr:=initDatabase(); err!=nil {
returnctxerrors.Wrap(err, "database initialization failed")
}
returnnil
}

Output shows the full chain with context from each wrap:

database initialization failed: failed to read config: config file missing [/path/to/server.go:12 in main.readConfig] [/path/to/server.go:17 in main.initDatabase] [/path/to/server.go:24 in main.startServer]

Stupid inline chaining

Or if you're a masochist and like one-liners:

funcclusterfuck() error {
returnctxerrors.Wrap(
ctxerrors.Wrap(
ctxerrors.New("original fuckup"),
"second layer of shit"),
"final layer of despair")
}

Output:

final layer of despair: second layer of shit: original fuckup [/path/to/file.go:42 in main.clusterfuck] [/path/to/file.go:41 in main.clusterfuck] [/path/to/file.go:40 in main.clusterfuck]

Unwrapping errors

You can unwrap the chain to get to the original error:

err:=startServer()
originalErr:=errors.Unwrap(errors.Unwrap(err))
// originalErr is now the "config file missing" error// Or use errors.Is() to check if specific error is in the chainiferrors.Is(err, someSpecificError) {
// handle it
}
// Or use errors.As() to check if it's a CTXErrorvarctxErr*ctxerrors.CTXErroriferrors.As(err, &ctxErr) {
fmt.Println("Got a CTXError:", ctxErr.Error())
}

No more guessing where the fuck everything went tits up.

More stupid fucking examples

Annoyingly complex tangled bullshit

Because sometimes you write code like a fucking maniac:

funcprocessUserShitWithStupidNesting(userIDint) error {
validateUser:=func(idint) error {
ifid<=0 {
returnctxerrors.New("invalid user ID: must be greater than zero")
}
returnnil
}
fetchUserData:=func(idint) error {
ifrand.Intn(3) ==0 {
returnctxerrors.Wrapf(
errors.New("connection timeout"),
"failed to fetch user data for user ID %d", id)
}
returnnil
}
processPermissions:=func(idint) error {
checkAdminRights:=func() error {
ifrand.Intn(2) ==0 {
returnctxerrors.New("user lacks admin privileges")
}
returnnil
}
iferr:=checkAdminRights(); err!=nil {
returnctxerrors.Wrapf(err, "permission check failed for user %d", id)
}
returnnil
}
// Chain all this shit togetheriferr:=validateUser(userID); err!=nil {
returnctxerrors.Wrap(err, "user validation step failed")
}
iferr:=fetchUserData(userID); err!=nil {
returnctxerrors.Wrap(err, "data fetching step failed")
}
iferr:=processPermissions(userID); err!=nil {
returnctxerrors.Wrap(err, "permission processing step failed")
}
returnnil
}

When this clusterfuck fails, you get a beautiful trace:

data fetching step failed: failed to fetch user data for user ID 42: connection timeout [main.go:15 in processUserShitWithStupidNesting.func2] [main.go:35 in processUserShitWithStupidNesting]

Ridiculously stupid chain of doom

For when you really want to piss off your future self:

funcperformStupidlyComplexOperation() error {
// Because apparently we hate ourselves and everyone who reads this codereturnfunc() error {
// Welcome to nested function hell, population: youiferr:=func() error {
// This is where sanity comes to dieiferr:=func() error {
// At this point we're just fucking with peopleiferr:=func() error {
// The beginning of the endiferr:=func() error {
// Rock bottom of this shitshowreturnctxerrors.New("step 1 went to shit")
}(); err!=nil {
// Step 2: electric boogaloo of failurereturnctxerrors.Wrap(err, "step 2 couldn't handle step 1's bullshit")
}
// If we somehow made it this far, we're lyingreturnnil
}(); err!=nil {
// Step 3: the reckoningreturnctxerrors.Wrap(err, "step 3 is having a mental breakdown")
}
// Still pretending everything is finereturnnil
}(); err!=nil {
// Step 4: fuck it, we're done tryingreturnctxerrors.Wrap(err, "step 4 said fuck this shit")
}
// The calm before the stormreturnnil
}(); err!=nil {
// The final boss of this clusterfuckreturnctxerrors.Wrap(err, "the entire fucking operation is fucked")
}
// Narrator: it was not finereturnnil
}() // Because we needed one more layer of stupid
}

Output when everything goes to hell:

the entire fucking operation is fucked: step 4 said fuck this shit: step 3 is having a mental breakdown: step 2 couldn't handle step 1's bullshit: step 1 went to shit [main.go:42 in step1] [main.go:47 in step2] [main.go:53 in step3] [main.go:59 in step4] [main.go:65 in performStupidlyComplexOperation]

This shit makes debugging actually bearable instead of wanting to throw your laptop out the fucking window.

Error mapping

Stop foreign errors from leaking through your layers. Register a translation map at startup and Wrap/Wrapf will swap matching driver errors for your own business errors before wrapping.

package main
import (
"errors""github.com/psyb0t/ctxerrors""gorm.io/gorm"
)
var (
ErrNotFound=errors.New("not found")
ErrAlreadyExists=errors.New("already exists")
)
funcinit() {
ctxerrors.SetErrorMap(map[error]error{
gorm.ErrRecordNotFound: ErrNotFound,
gorm.ErrDuplicatedKey: ErrAlreadyExists,
})
}
funcGetUser(idint) error {
err:=db.First(&user, id).Error// returns gorm.ErrRecordNotFoundiferr!=nil {
// wrapped err satisfies errors.Is(err, ErrNotFound) — gorm.ErrRecordNotFound is gonereturnctxerrors.Wrap(err, "get user")
}
returnnil
}

API:

  • SetErrorMap(map[error]error) — replace the whole map.
  • MapError(from, to error) — add/overwrite a single entry (good for init() per package).
  • ClearErrorMap() — wipe it (mostly for tests).

Matching uses errors.Is, so already-wrapped foreign errors still translate. Translation is single-pass — no chained A→B→C. nil keys/values are ignored. If no entry matches, behavior is unchanged.

License

MIT License - because lawyers are expensive and I don't want to deal with that shit

Why?

Because Go's error handling is verbose as fuck and debugging without context is like trying to find your asshole with both hands tied behind your back. This shit makes it slightly less painful.

About

A Go library that wraps errors with context information (file, line, function) because debugging without context is like trying to find your dick in the dark.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages