• Stars
    star
    148
  • Rank 241,197 (Top 5 %)
  • Language
    Go
  • License
    MIT License
  • Created almost 2 years ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Go errors but structured and composable. Fault provides an extensible yet ergonomic mechanism for wrapping errors.

header

GoDoc Go Report Card codecov

Fault provides an extensible yet ergonomic mechanism for wrapping errors. It implements this as a kind of middleware style pattern of simple functions called decorators: func(error) error. A decorator simply wraps an error within another error, much like many libraries do.

What this facilitates is a simple, minimal and (most important) composable collection of error handling utilities designed to help you diagnose problems in your application logic without the need for overly verbose stack traces.

This is achieved by annotating errors with structured metadata instead of just gluing strings of text together. This approach plays nicely with structured logging tools as well as the existing Go errors ecosystem.

Usage

You can gradually adopt Fault into your codebase as it plays nicely with the existing Go error management ecosystem.

Wrapping errors

Wrapping errors as you ascend the call stack is essential to providing your team with adequate context when something goes wrong.

Simply wrap your errors as you would with any library:

if err != nil {
    return fault.Wrap(err)
}

What this gives you is basic stack traces. The philosophy behind stack traces provided by Fault is that you only care about relevant code locations. You do not need runtime/proc.go in your stack traces unless you're actually working on the Go compiler or some crazy low level tooling. A fault stack trace looks like this when formatted with %+v:

stdlib sentinel error
    /Users/southclaws/Work/fault/fault_test.go:34
failed to call function
    /Users/southclaws/Work/fault/fault_test.go:43
    /Users/southclaws/Work/fault/fault_test.go:52

And of course all of this information is accessible in a structured way so you can serialise it how you want for your logging stack of choice. Fault aims to be unopinionated about presentation.

But what if you want to add context? With pkg/errors and similar libraries, you often use errors.Wrap(err, "failed to do something") to add a bit of context to a wrapped error.

Fault provides something much more powerful, a mechanism to compose many wrappers together. The Wrap API in Fault accepts any number of functions that wrap the error with the signature func(error) error so the possibilities are endless.

if err != nil {
    return fault.Wrap(err, fmsg.With("failed to do a thing"))
}

This is how you add a message to an error in the same way you would with pkg/errors. Now this is quite verbose, lots of typing involved and if this is all you're going to do, you don't need to use Fault, you can just use fmt.Errorf and %w.

The power comes when you make use of some of the additional packages available as part of Fault:

if err != nil {
    return fault.Wrap(err,
        fctx.With(ctx), // decorate the error with key-value metadata from context
        ftag.With(ftag.NotFound), // categorise the error as a "not found"
        fmsg.With("failed to do something", "There was a technical error while retrieving your account"), // provide an end-user message
    )
}

You can also build your own utilities that work with the Fault option pattern. This is covered later in this document.

Handling errors

Wrapping errors is only half the story, eventually you'll need to actually handle the error (and no, return err is not "handling" an error, it's saying "I don't know what to do! Caller, you deal with this!".)

Utilities will provide their own way of extracting information from an error but Fault provides the Flatten function which allows you to access a simple stack trace and a reference to the root cause error.

chain := Flatten(err)

chain.Root

This is the root cause of the error chain. In other words, the error that was either created with errors.New (or similar) or some external error from another library not using Fault.

chain.Errors

This is the list of wrapped errors in the chain where the first item is the wrapper of the root cause.

error chain diagram

Utilities

Fault provides some utilities in subpackages to help you annotate and diagnose problems easily. Fault started its life as a single huge kitchen-sink style library but it quickly became quite bloated and developers rarely used everything it provided. This inspired the simple modular option-style design and each useful component was split into its own package.

It's worth noting that all of these utilities can be used on their own. If you don't want to use Fault's wrapping or stack traces you can simply import a library and use its Wrap function.

fmsg

end-user error example

This simple utility gives you the ability to decorate error chains with a separate set of messages intended for both developers and end-users to read.

The error messages returned by .Error() are always intended for developers to read. They are rarely exposed to end-users. When they are, it's usually fairly confusing and not a great user-experience.

You can use fmsg.With to wrap an error with an extra string of text, just like pkg/errors and similar:

err := errors.New("root")
err = fault.Wrap(err, fmsg.With("one"))
err = fault.Wrap(err, fmsg.With("two"))
err = fault.Wrap(err, fmsg.With("three"))
fmt.Println(err)
// three: two: one: root

As with any simple error wrapping library, the .Error() function simply joins these messages together with :.

However, in order to provide useful end-user error descriptions, you can use fmsg.WithDesc:

if err != nil {
    return fault.Wrap(err,
        fmsg.WithDesc("permission denied", "The post is not accessible from this account."),
    )
}

Once you're ready to render the human-readable errors to end-users, you simply call GetIssue:

issues := GetIssue(err)
// "The post is not accessible from this account."

Multiple wrapped issues are conjoined with a single space. Issue messages should end in a punctuation mark such as a period.

if err != nil {
    return fault.Wrap(err,
        fmsg.With("permission denied", "The category cannot be edited."),
    )
}

// later on

if err != nil {
    return fault.Wrap(err,
        fmsg.With("move post failed", "Could not move post to the specified category."),
    )
}

Yields:

issues := GetIssue(err)
// "Could not move post to the specified category. The category cannot be edited."

Which, while it reads much nicer than move post failed: permission denied, both messages are valuable to their individual target audiences.

Further reading on the topic of human-friendly error messages in this article.

fctx

fctx example

Error context is the missing link between the context package and errors. Contexts often contain bits of metadata about a call stack in large scale applications. Things like trace IDs, request IDs, user IDs, etc.

The problem is, once an error occurs and the stack starts to "unwind" (chained returns until the error is truly "handled" somewhere) the information stored in a context has already gone.

fctx gives you two tools to help understand the actual context around a problem.

Add metadata to a context

First, you decorate contexts with key-value data. Strings only and no nesting for simplicity.

ctx = fctx.WithMeta(ctx, "trace_id", traceID)

This stores the traceID value into the context. Conflicting keys will overwrite.

Note that while this function may look similar to context.WithValue in concept, it differs since it permits you to access all of the key-value as a single object for iterating later. When using context.WithValue you must know the exact keys. Now you could store a map but in order to add items to that map you would need to first read the map out, check if it exists, insert your key-value data and write it back.

Decorate errors with context metadata

When something goes wrong, all that metadata stored in a context can be copied into an error.

if err != nil {
    return fault.Wrap(err,
        fctx.With(ctx),
    )
}

Access metadata for logging

When your error chain is handled, you most likely want to log what happened. You can access key-value metadata from an error using Unwrap:

ec := fctx.Unwrap(err)
logger.Error("http handler failed", ec)

The Unwrap function returns a simple map[string]string value which you can use with your favourite structured logging tool. Now, instead of your logs looking like this:

{
  "error": "failed to create post 'My new post' in thread '1647152' by user '@southclaws' database connection refused"
}

Which is an absolute nightmare to search for when every error is worded differently...

They look like this:

{
  "error": "failed to create post: database connection refused",
  "metadata": {
    "title": "My new post",
    "thread_id": "1647152",
    "user_id": "@southclaws"
  }
}

Which is an absolute godsend when things go wrong.

ftag

This utility simply annotates an entire error chain with a single string. This facilitates categorising error chains with a simple token that allows mapping errors to response mechanisms such as HTTP status codes or gRPC status codes.

You can use the included error kinds which cover a wide variety of common categories of error or you can supply your own set of constants for your team/codebase.

if err != nil {
    return fault.Wrap(err,
        ftag.With(ftag.NotFound),
    )
}

Once you've annotated an error chain, you can use it later to determine which HTTP status to respond with:

ek := ftag.Get(err)
// ftag.NotFound = "NOT_FOUND"

switch ek {
  case ftag.NotFound:
    return http.StatusNotFound
  // others...
  default:
    return http.StatusInternalServerError
}

This removes the need to write verbose and explicit errors.Is checks on the error type to determine which type of HTTP status code to respond with.

Since the type Kind is just an alias to string, you can pass anything and switch on it.

Appendix

Rationale

The reason Fault came into existence was because I found nesting calls to various Wrap APIs was really awkward to write and read. The Golang errors ecosystem is diverse but unfortunately, composing together many small error related tools remains awkward due to the simple yet difficult to extend patterns set by the Golang standard library and popular error packages.

For example, to combine pkg/errors, tracerr and fctx you'd have to write:

fctx.Wrap(errors.Wrap(tracerr.Wrap(err), "failed to get user"), ctx)

Which is a bit of a nightmare to write (many nested calls) and a nightmare to read (not clear where the arguments start and end for each function). Because of this, it's not common to compose together libraries from the ecosystem.

Prior art

Building on the shoulders of giants, as is the open source way. Here are some great libraries I goodartistscopygreatartistssteal'd from:

Why "Fault"?

Because the word error is overused.

errors everywhere

More Repositories

1

sampctl

The Swiss Army Knife of SA:MP - vital tools for any server owner or library maintainer.
Go
238
star
2

ScavengeSurvive

A PvP SA:MP survival gamemode. The aim of the game is to find supplies such as tools or weapons to help you survive, either alone or in a group.
Pawn
101
star
3

restic-robot

Backups done right... by robots! Restic backup but the robot friendly version.
Go
72
star
4

storyden

With a fresh new take on traditional bulletin board forum software, Storyden is a modern, secure and extensible platform for building communities.
TypeScript
69
star
5

pawn-requests

pawn-requests provides an API for interacting with HTTP(S) JSON APIs.
Pawn
65
star
6

progress2

A SA:MP UI library for rendering progress bars used to visualise all manner of data from health to a countdown timer.
Pawn
59
star
7

supervillain

Converts Go structs to Zod schemas
Go
58
star
8

vscode-pawn

Pawn tools for vscode, powered by sampctl.
TypeScript
47
star
9

cj

CJ is a Discord bot that hangs around in the open.mp/burgershot.gg community discord.
Go
39
star
10

pawn-redis

Redis client for the Pawn language
C++
38
star
11

opt

A simple and ergonomic optional type for Go.
Go
34
star
12

pawn-sublime-language

Pawn language settings for Sublime Text 3. Copied from C++ but with Pawn language and SA:MP specific modifications.
Python
29
star
13

SIF

SIF is a collection of high-level include scripts to make the development of interactive features easy for the developer while maintaining quality front-end gameplay for players.
Pawn
26
star
14

samp-Hellfire

My long running San Andreas Multiplayer project, I aim to fill this gamemode script with as much as possible to accommodate for all player's tastes!
Pawn
24
star
15

pawn-json

JSON for Pawn.
Rust
22
star
16

samp-servers-api

Deprecated: use https://open.mp/servers and https://api.open.mp/servers now
Go
22
star
17

samp-aviation

A basic pitch-based altitude and roll-based heading autopilot for SA-MP. Based on real autopilot behaviour with some adjustments made for the simple physics of San Andreas.
Pawn
21
star
18

samp-logger

Structured logging for Pawn.
Pawn
18
star
19

pawn-uuid

A Pawn plugin that provides a simple UUID version 4 generator function.
C++
16
star
20

pawn-chrono

A modern Pawn library for working with dates and times.
C++
15
star
21

forumfmt

A personal tool for converting from Markdown to BBCode for SA:MP forum.
Go
13
star
22

samp-geoip

A simple library that provides information from IPHub for connected players.
Pawn
13
star
23

pawn-parser

Derived from the Golang scanner/token packages and modified for Pawn code.
Go
13
star
24

samp-weapon-data

With this library you can finely tune weapon damage based on distance. Using min/max range values, a weapon's damage varies depending on the distance between the shooter and the target.
Pawn
13
star
25

sliding-window-counters

Sliding window counters Redis rate limiting implementation for Golang (Based on the Figma API rate limit algorithm)
Go
12
star
26

pawn-env

Provides access to environment variables in Pawn.
C++
12
star
27

result

Go generic result type and utilities
Go
12
star
28

pawn-errors

A minimal, C/Go-esque, error handling library for the Pawn language
Pawn
12
star
29

samp-plugin-boilerplate

Boilerplate setup for a SA:MP plugin - uses CMake, plugin-natives, Docker and sampctl
C++
11
star
30

enumerator

Generate safe and validated enumerated types.
Go
11
star
31

formatex

Slice's formatex because it doesn't have a GitHub repo
Pawn
10
star
32

prisment

Prisma to Ent schema conversion script
Go
10
star
33

go-cex

A Go library for accessing the CeX trade store products API
Go
9
star
34

Pawpy

Threaded Python utility plugin for SA:MP - unifying two of my favourite languages! Run threaded Python scripts from within a SA:MP script.
C++
9
star
35

modio

A binary file IO script designed specifically for modular SA:MP gamemodes.
Pawn
9
star
36

samp-zipline

Create fun and useful ziplines players can use to speed across large areas quickly. Warning: does not work well with laggy players.
Pawn
9
star
37

samp-animbrowse

Browse and search through the entire GTA:SA animation library with ease.
Pawn
8
star
38

pocket

A neat little web library to help you write cleaner HTTP request handlers!
Go
8
star
39

pawn-bcrypt

bcrypt for Pawn.
C++
7
star
40

pawndex

Pawn package list aggregator - uses the GitHub API to find Pawn packages for sampctl
Go
7
star
41

clawsh

A modern shell that breaks all the rules.
Rust
7
star
42

textnot.pictures

Info site for people who post screenshots of text when asking for help. inspired by dontasktoask.com
HTML
7
star
43

pawn-fsutil

fsutil is a file system utility plugin for the Pawn language
C++
7
star
44

thanks

A Go equivalent of github.com/feross/thanks ✨
Go
6
star
45

samp-plugin-mapandreas

C++
6
star
46

samp-qr

Does QR codes, renders them as a grid of pool balls.
Pawn
6
star
47

zcmd

This is merely a GitHub repost of zcmd by @Zeex because it does not exist on GitHub making package management with sampctl difficult.
Pawn
6
star
48

samp-linegen

Generates a line of objects between start point and destination. Useful for ziplines, tunnels, police tape, funky infinite neon strips, etc.
Pawn
6
star
49

samp-object-loader

A simple yet powerful and easy to use map parser for SA:MP. Reads 'CreateObject' (and any varient) lines from .map files with recursive directory listing. Supports RemoveBuildingForPlayer as well as materials and material text.
Pawn
6
star
50

go-hexagonal-architecture

An actually good production ready hexagonal architecture explanation!
5
star
51

homepage

My homepage built with React and Next.js.
JavaScript
5
star
52

pawn-templates

Template rendering for Pawn.
Rust
5
star
53

samp-nolog

SA:MP server plugin to prevent writing to server_log.txt
CMake
5
star
54

samp-objects-api

https://samp-objects.com Backend API service - handles user authentication, uploads from FineUploader and object search queries.
Go
5
star
55

samp-bitmapper

For generating in-game coordinates from a bitmap.
C++
5
star
56

samp-whirlpool

Fork of the Whirlpool cryptography SA:MP plugin originally Y_Less.
Objective-C
5
star
57

samp-prophunt

A SA:MP gamemode inspired by the popular Team Fortress 2 mod "PropHunt" by Darkimmortal.
Pawn
5
star
58

rst

The Resource-Service-Transport system design approach
5
star
59

samp-ini

A simple cache based ini format file parser, stores file contents in memory to manipulate in order to minimise actual file operations.
Pawn
5
star
60

pdf_extractor

Extracts text from PDF files. Utilises multiple cores, does one page on one core at a time.
Python
4
star
61

dockwatch

Go library for watching Docker containers for changes.
Go
4
star
62

samp-camera-sequencer

A library for creating camera sequences using files to store the coordinates and sequence data. A camera sequence is a set of camera nodes and can be loaded from a file created by the editor. Each camera node consists of coordinates and timing data. Comes packaged with an easy to use editor for creating cinematic camera sequences.
Pawn
4
star
63

pawn-yaml

YAML for Pawn
Pawn
4
star
64

samp-rediscord

SA:MP to Discord plugin built with Redis as the bridge.
Go
4
star
65

cordless-old

Discord but the 1980s terminal version.
Go
4
star
66

pawn-levenshtein

Levenshtein distance package for Pawn.
Pawn
4
star
67

wordpress-to-markdown

Convert a wordpress exported XML file to markdown files for Jekyll
Python
4
star
68

codeblockplease

Info site for people who post code without using formatting when asking for help. inspired by dontasktoask.com
HTML
4
star
69

pawndex-frontend

Frontend React app for the Pawndex Pawn Package Indexing API
JavaScript
4
star
70

flow

dt = data transformers
Go
4
star
71

samp-objects-frontend

https://samp-objects.com Frontend application - React app providing an interface to the samp-objects-api service.
TypeScript
4
star
72

qstring

This package provides an easy way to marshal and unmarshal url query string data to and from structs.
Go
3
star
73

fnm-nushell

fnm -> this -> load-env = use fnm in nushell
Go
3
star
74

OnPlayerSlowUpdate

Like OnPlayerUpdate... but slower - every 100ms.
Pawn
3
star
75

yaps

yet another paste site
Rust
3
star
76

gitwatch

Simple Go library for detecting changes in remote Git repositories
Go
3
star
77

pawn-fmt

fmtlib for Pawn
C++
3
star
78

pawn-requests-example

This simple gamemode demonstrates how to use the pawn-requests plugin with jsonstore.io to store player data.
Pawn
3
star
79

invision-community-go

Golang client for the Invision Community forum API
Go
3
star
80

uptime-girl

🎡 She's an uptime girl, she's been living in an uptime world! 🎡 - seriously: an Uptime Robot robot that automatically creates monitors based on container labels.
Go
3
star
81

samp-ladders

Create simple ascend / descend points in your levels where players can move directly up or down. The animation isn't great and looks a bit stupid, but it's the one I thought looked best!
Pawn
3
star
82

pawn-ctime

The original CTime plugin by RyDeR`, with some major stability and quality improvements.
C++
3
star
83

logctx

Package logctx provides a way to decorate structured log entries with metadata added to a `context.Context`.
Go
3
star
84

pawn-package-template

A boilerplate template repository for a Pawn Package. If you're writing a new package, clone this repo as a starting point!
Pawn
3
star
85

bob-the-builder

Just a league of legends team builder.
Go
2
star
86

content-fullpage-scroll

JavaScript
2
star
87

homebrew-sampctl

Homebrew tap for https://github.com/Southclaws/sampctl
Ruby
2
star
88

gta-chaos-discord

Go
2
star
89

modio-py

Python implementation of a reader/writer for the modio binary file format.
Python
2
star
90

watchgraph

`watch` with a graph!
Go
2
star
91

nullable

Go
2
star
92

imagegrid

An example of an image grid using a css-grid layout with (almost) automated distribution and dense packing.
CSS
2
star
93

darkmodescience

An info page about dark mode.
JavaScript
2
star
94

fx-example

An example of how to use Uber's fx library.
Go
2
star
95

go-samp-query

SA:MP Query API for Go
Go
2
star
96

samp-servers-frontend

ReactJS frontend for the http://samp-servers.net RESTful API (https://github.com/Southclaws/samp-servers-api)
JavaScript
2
star
97

machinehead

A docker-compose application manager that deploys and maintains a set of compose projects and provides secret management for them via Vault.
Go
2
star
98

tickerpool

A worker pool of timed tasks, balanced equally to prevent cpu spikes.
Go
2
star
99

samp-attachedit

An object attachment editor for SA:MP. Easy editing of attached objects using the SA:MP client-side on-screen controls.
Pawn
2
star
100

IDArling-docker

Docker image and compose config for IDArling
Dockerfile
2
star