From 412c2f48ea9551a935e093b26584514ebe401699 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Wed, 22 Jul 2026 18:14:22 +0200 Subject: [PATCH] feat: better error handling --- .gitignore | 1 + errors.go | 107 ++++++++++++++++++++++++++++++++++++++ examples/errors/README.md | 5 ++ examples/errors/errors.go | 37 +++++++++++++ examples/errors/go.mod | 7 +++ examples/errors/go.sum | 0 6 files changed, 157 insertions(+) create mode 100644 errors.go create mode 100644 examples/errors/README.md create mode 100644 examples/errors/errors.go create mode 100644 examples/errors/go.mod create mode 100644 examples/errors/go.sum diff --git a/.gitignore b/.gitignore index 6b47311..c2b59d4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ examples/connect-desktop/build examples/connect-desktop/frontend/dist examples/connect-desktop/frontend/wailsjs examples/connect/connect +examples/errors/errors examples/pairchat/pairchat examples/timeserve/timeserve iroh-go/target/ diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..6ed8b3f --- /dev/null +++ b/errors.go @@ -0,0 +1,107 @@ +package iroh_ffi + +import ( + "errors" + "fmt" +) + +// As casts an error to IrohError and back, extracting relevant error message +// information. It is a single argument variant of the typical errors.As API +// because we always expect an IrohError back from the iroh FFI bindings. The +// bindings do not return an IrohError for reasons and will not for the +// forseeable future due to API stability guarantees. +func As(err error) error { + if err, ok := errors.AsType[IrohError](err); ok { + return errors.New(fmt.Sprintf("%s: %s", err.Message(), err.DebugMessage())) + } + return err +} + +// Is suports matching against the stable iroh error taxonomy using the error +// sentinel construction below. The caller must pass in a valid error sentinel +// specified by this library, e.g. iroh.BindError. +func (e *IrohError) Is(target error) bool { + if err, ok := errors.AsType[sentinel](target); ok { + return uint(e.Kind()) == err.kind + } + return false +} + +type sentinel struct { + kind uint + message string +} + +func (s sentinel) Error() string { + return s.message +} + +var InvalidInputError error = sentinel{ + kind: uint(IrohErrorKindInvalidInput), + message: "iroh: invalid input supplied by the caller", +} + +var BindError error = sentinel{ + kind: uint(IrohErrorKindBind), + message: "iroh: failure while initiating or completing an outgoing connection", +} + +var ConnectError error = sentinel{ + kind: uint(IrohErrorKindConnect), + message: "iroh: failure while initiating or completing an outgoing connectio", +} + +var ConnectionError error = sentinel{ + kind: uint(IrohErrorKindConnection), + message: "iroh: an established connection failed or closed unexpectedly", +} + +var AlpnError error = sentinel{ + kind: uint(IrohErrorKindAlpn), + message: "iroh: ALPN negotiation or lookup failed", +} + +var KeyParsingError error = sentinel{ + kind: uint(IrohErrorKindKeyParsing), + message: "iroh: endpoint id / public key parsing failed", +} + +var TicketParsingError error = sentinel{ + kind: uint(IrohErrorKindKeyParsing), + message: "iroh: ticket parsing failed", +} + +var RelayError error = sentinel{ + kind: uint(IrohErrorKindRelay), + message: "iroh: stream read/write/control operation failed", +} + +var StreamError error = sentinel{ + kind: uint(IrohErrorKindStream), + message: "iroh: datagram send/receive operation failed", +} + +var DatagramError error = sentinel{ + kind: uint(IrohErrorKindDatagram), + message: "iroh: datagram send/receive operation failed", +} + +var FCallbackError error = sentinel{ + kind: uint(IrohErrorKindCallback), + message: "iroh: foreign callback failed", +} + +var ClosedError error = sentinel{ + kind: uint(IrohErrorKindClosed), + message: "iroh: operation was attempted on a closed stream/connection/resource", +} + +var TimeoutError error = sentinel{ + kind: uint(IrohErrorKindTimeout), + message: "iroh: operation timed out", +} + +var InternalError error = sentinel{ + kind: uint(IrohErrorKindInternal), + message: "iroh: unclassified internal error", +} diff --git a/examples/errors/README.md b/examples/errors/README.md new file mode 100644 index 0000000..09cec99 --- /dev/null +++ b/examples/errors/README.md @@ -0,0 +1,5 @@ +# errors + +How to deal with error handling in a convenient way. See +[`#263`](https://github.com/n0-computer/iroh-ffi/issues/263) for the juicy +details. diff --git a/examples/errors/errors.go b/examples/errors/errors.go new file mode 100644 index 0000000..40333fd --- /dev/null +++ b/examples/errors/errors.go @@ -0,0 +1,37 @@ +package main + +import ( + "errors" + + iroh "git.coopcloud.tech/decentral1se/iroh-go" +) + +func main() { + preset := iroh.PresetN0DisableRelay() + opts := iroh.EndpointOptions{ + Preset: &preset, + Alpns: &[][]byte{[]byte("iroh-go-errors/0")}, + } + + endpoint, err := iroh.EndpointBind(opts) + + // BEGIN API PREVIEW ZONE + + // 1. convert error to IrohError with As + // bring IrohError.Message/DebugMessage back + // maintain `err != nil` compatibility + if err := iroh.As(err); err != nil { + panic(err) + } + + // 1. no As machinery required + // sentinel check with errors.Is support + // works like usual `err` + if err != nil && errors.Is(err, iroh.BindError) { + panic(err) + } + + // END API PREVIEW ZONE + + endpoint.Online() +} diff --git a/examples/errors/go.mod b/examples/errors/go.mod new file mode 100644 index 0000000..811614a --- /dev/null +++ b/examples/errors/go.mod @@ -0,0 +1,7 @@ +module git.coopcloud.tech/decentral1se/iroh-go/examples/errors + +go 1.26.1 + +require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25 + +replace git.coopcloud.tech/decentral1se/iroh-go => ../../ diff --git a/examples/errors/go.sum b/examples/errors/go.sum new file mode 100644 index 0000000..e69de29 -- 2.52.0