exc

package
v0.6.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 18, 2022 License: BSD-3-Clause Imports: 5 Imported by: 4

README

Exceptions for Go as a Library

by Tim Henderson (tim.tadh@gmail.com)

Copyright 2016, Licensed under the GPL version 2. Please reach out to me directly if you require another licensing option. I am willing to work with you.

Documentation

Explanation

Go (golang) lacks support for exceptions found in many other languages. There are good reasons for Go to not include exceptions. For instance, by making error handling explicit the programmer is forced to think concretely about the correct action to take. Fined grained control over the handling of errors using multiple return parameters is one of Go's strengths.

However, there are cases where Go programs do not universally benefit from the explicit handling of errors. For instance, consider the following code:

func DoStuff(a, b, c interface{}) error {
	x, err := foo(a)
	if err != nil {
		return err
	}
	y, err := bar(b, x)
	if err != nil {
		return err
	}
	z, err := bax(c, x, y)
	if err != nil {
		return err
	}
	return baz(x, y, z)
}

If Go had exceptions such code could be easily simplified:

func DoStuff(a, b, c interface{}) throws error {
	x := foo(a)
	y := bar(b, x)
	baz(x, y, bax(c, x, y)
}

This library allow you to write go with exceptions and try-catch-finally blocks. It is not appropriate for all situations but can simplify some application code. Libraries and external APIs should continue to conform to the Go standard of returning error values.

Here is an example of the DoStuff function where foo, bar and baz all throw exceptions instead of returning errors. (We will look at the case where they return errors that you want to turn into exceptions next). We want DoStuff to be an public API function and return an error:

func DoStuff(a, b, c interface{}) error {
	return exc.Try(func() {
		x := foo(a)
		y := bar(b, x)
		baz(x, y, bax(c, x, y)
	}).Error()
}

Now let's consider the case where we want to catch the exception log and reraise it:

func DoStuff(a, b, c interface{}) error {
	return exc.Try(func() {
		x := foo(a)
		y := bar(b, x)
		baz(x, y, bax(c, x, y)
	}).Catch(&exc.Exception{}, func(t exc.Throwable) {
		log.Log(t)
		exc.Rethrow(t, exc.Errorf("rethrow after logging"))
	}).Error()
}

Rethrow will chain the Throwable t with the new *Error created such that if/when the exception reaches the top level you know exactly how it was created and where it was rethrown.

Ok, what about interacting with regular Go APIs which return errors? How can we turn those errors into exceptions? The easy was is to use the ThrowOnError function which is a sugar for:

if err != nil {
	ThrowErr(ErrorFrom(err)
}

So converting out original DoStuff function we get

func DoStuff(a, b, c interface{}) { // Throws
	x, err := foo(a)
	exc.ThrowOnError(err)
	y, err := bar(b, x)
	exc.ThrowOnError(err)
	z, err := bax(c, x, y)
	exc.ThrowOnError(err)
	exc.ThrowOnError(baz(x, y, z))
}

This package also supports: catching user defined exceptions, catching multiple exception types, Close which works like the "try with resources" construct in Java 7+, (multiple) finally blocks, and a choice between propogating exceptions with Unwind or retrieving the error/exception with Error and Exception functions.

One Gotcha! The Try() function creates a *Block struct. To execute the block you must either call: Unwind, Error, or Exception. Unwind executes the block, if there is an exception coming out of the block it continues to cause the program stack unwind. Error and Exception excute the block, but return the exception as a value to deal with in the usual Go way.

Documentation

Overview

Exceptions for Go as a Library

Explanation: http://hackthology.com/exceptions-for-go-as-a-library.html

Go (golang) lacks support for exceptions found in many other languages. There are good reasons for Go to not include exceptions. For instance, by making error handling explicit the programmer is forced to think concretely about the correct action to take. Fined grained control over the handling of errors using multiple return parameters is one of Go's strengths.

However, there are cases where Go programs do not universally benefit from the explicit handling of errors. For instance, consider the following code:

func DoStuff(a, b, c interface{}) error {
	x, err := foo(a)
	if err != nil {
		return err
	}
	y, err := bar(b, x)
	if err != nil {
		return err
	}
	z, err := bax(c, x, y)
	if err != nil {
		return err
	}
	return baz(x, y, z)
}

If Go had exceptions such code could be easily simplified:

func DoStuff(a, b, c interface{}) throws error {
	x := foo(a)
	y := bar(b, x)
	baz(x, y, bax(c, x, y)
}

This library allow you to write go with exceptions and try-catch-finally blocks. It is not appropriate for all situations but can simplify some application code. Libraries and external APIs should continue to conform to the Go standard of returning error values.

Here is an example of the `DoStuff` function where foo, bar and baz all throw exceptions instead of returning errors. (We will look at the case where they return errors that you want to turn into exceptions next). We want DoStuff to be an public API function and return an error:

func DoStuff(a, b, c interface{}) error {
	return exc.Try(func() {
		x := foo(a)
		y := bar(b, x)
		baz(x, y, bax(c, x, y)
	}).Error()
}

Now let's consider the case where we want to catch the exception log and reraise it:

func DoStuff(a, b, c interface{}) error {
	return exc.Try(func() {
		x := foo(a)
		y := bar(b, x)
		baz(x, y, bax(c, x, y)
	}).Catch(&exc.Exception{}, func(t exc.Throwable) {
		log.Log(t)
		exc.Rethrow(t, exc.Errorf("rethrow after logging"))
	}).Error()
}

Rethrow will chain the Throwable `t` with the new `*Error` created such that if/when the exception reaches the top level you know exactly how it was created and where it was rethrown.

Ok, what about interacting with regular Go APIs which return errors? How can we turn those errors into exceptions? The easy was is to use the `ThrowOnError` function which is a sugar for:

if err != nil {
	ThrowErr(ErrorFrom(err)
}

So converting out original `DoStuff` function we get

func DoStuff(a, b, c interface{}) { // Throws
	x, err := foo(a)
	exc.ThrowOnError(err)
	y, err := bar(b, x)
	exc.ThrowOnError(err)
	z, err := bax(c, x, y)
	exc.ThrowOnError(err)
	exc.ThrowOnError(baz(x, y, z))
}

This package also supports: catching user defined exceptions, catching multiple exception types, `Close` which works like the "try with resources" construct in Java 7+, (multiple) finally blocks, and a choice between propogating exceptions with `Unwind` or retrieving the error/exception with `Error` and `Exception` functions.

One Gotcha! The `Try()` function creates a `*Block` struct. To execute the block you must either call: `Unwind`, `Error`, or `Exception`. `Unwind` executes the block, if there is an exception coming out of the block it continues to cause the program stack unwind. `Error` and `Exception` excute the block, but return the exception as a value to deal with in the usual Go way.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Rethrow

func Rethrow(e Throwable, err *Error)

Rethrow an object. It chains on the *Error as the reason for the rethrow and where it occured. If you are inside of a catch block you should use this method instead of Throw*

exc.Try(func() {
	exc.Throwf("wat!@!")
}).Catch(&Exception{}, func(e Throwable) {
	t.Log("Caught", e)
	exc.Rethrow(e, Errorf("rethrow"))
}).Unwind()

func Throw

func Throw(e Throwable)

Throw a Throwable object. This is how you throw a custom exception:

type MyException struct {
	exc.Exception
}

exc.Try(func() {
	exc.Throw(&MyException{*Errorf("My Exception").Exception()})
}).Catch(&Exception{}, func(t Throwable) {
	log.Log("caught!")
}).Unwind()

func ThrowErr

func ThrowErr(e *Error)

Throw an *Exception created from an *Error.

func ThrowOnError

func ThrowOnError(err error)

Throw an *Exception created from a regular error interface object. The stack trace for the exception will point to the throw rather than the creation of the error object.

func Throwf

func Throwf(format string, args ...interface{})

Throw a new *Exception created from an *Error made with Errorf. Basically a drop in replacement for everywhere you used fmt.Errorf but would now like to throw an exception without creating a custom exception type.

Types

type Block

type Block struct {
	// contains filtered or unexported fields
}

Represents a Try/Catch/Finally block. Created by a Try or Close function. Executed by Unwind, Error, or Exception. You may add multiple Catch and Finally expressions to your *Block with the Catch and Finally functions. Catch and Finally can be in arbitrary orders unlike in languages with syntatic support for Exceptions. I recommend you put your Catch expressions before your finally Expressions. Finally and Catch expressions are evaluated in the order they are declared.

func Close

func Close(makeCloser func() io.Closer, try func(io.Closer)) *Block

Start a Try/Catch/Finally block. You supply to functions, the first one creates a closable resource and returns it. This resources is supplied to the the second function which acts as a normal try. Whether the try fails or succeeds the Close() function is always called on the resource that was created (and returned) by the first function. Further, catch and finally functions may be chained onto the Close function. However, they will be run after the Close function is called on the resource and will not have access to it.

Finally, if the function to created the closable object fails, it will not be cleaned up if it was partially initialized. This is because it was never returned. There are also ways to deal with that situation using an outer Try/Finally.

func Try

func Try(try func()) *Block

Start a Try/Catch/Finally block. Any exception thrown in the Try block can be caught by a Catch block (if registered for the Exception type or a parent exception type).

func (*Block) Catch

func (b *Block) Catch(exc Throwable, do func(Throwable)) *Block

Add a catch function for a specific Throwable. If your Throwable struct "inherits" from another struct like so:

type MyException struct {
	exc.Exception
}

Then you can catch *MyException with *Exception. eg:

exc.Try(func() {
	Throw(&MyException{*Errorf("My Exception").Exception()})
}).Catch(&Exception{}, func(t Throwable) {
	log.Log("caught!")
}).Unwind()

Catch blocks are only run in the case of an thrown exception. Regular panics are ignored and will behave as normal.

func (*Block) Error

func (b *Block) Error() error

Run the Try/Catch/Finally *Block. If there is an uncaught (or rethrown) exception continue return it as an error.

The Block will NOT BE RUN unless this method, Unwind, or Exception is called. This could lead to an difficult to track down bug!

func (*Block) Exception

func (b *Block) Exception() Throwable

Run the Try/Catch/Finally *Block. If there is an uncaught (or rethrown) exception continue return it as a Throwable.

The Block will NOT BE RUN unless this method, Unwind, or Error is called. This could lead to an difficult to track down bug!

func (*Block) Finally

func (b *Block) Finally(finally func()) *Block

Add a finally block. These will be run whether or not an exception was thrown. However, if a regular panic occurs this function will not be run and the panic will behave as normal.

func (*Block) Unwind

func (b *Block) Unwind()

Run the Try/Catch/Finally *Block. If there is an uncaught (or rethrown) exception continue to propogate it up the stack (unwinding it). This would be the normal behavoir in language which natively support exceptions.

The Block will NOT BE RUN unless this method, Error, or Exception is called. This could lead to an difficult to track down bug!

type Error

type Error struct {
	Err   error
	Stack []byte
}

The *Error struct wraps up a regular error (could be any error) and a stack trace. The idea is that it track where the error was created.

func Errorf

func Errorf(format string, args ...interface{}) *Error

A drop in replacement for either fmt.Errorf or errors.Errorf. Creates and *Error with a stack trace from where Errorf() was called from.

func FromError

func FromError(err error) *Error

Create an *Error from an existing error value. It will create a stack trace to attach to the error from where FromError() was called from.

func (*Error) Error

func (e *Error) Error() string

Format the error and stack trace.

func (*Error) Exception

func (e *Error) Exception() *Exception

Create an Exception object from the Error.

func (*Error) String

func (e *Error) String() string

Format the error and stack trace.

type Exception

type Exception struct {
	Errors []*Error
}

An implementation of Throwable you can base your custom Exception types off of. It is also the type of Throwable thrown by Throwf. To "inherit" from Exception use this formula:

type MyException struct {
	exc.Exception
}

This ensures that your new exception will be catchable when *Exception is supplied. See *Block.Catch for details.

func (*Exception) Chain

func (e *Exception) Chain(err *Error) Throwable

Add another *Error to the list of *Error

func (*Exception) Error

func (e *Exception) Error() string

Format an error string from the list of *Error.

func (*Exception) Exc

func (e *Exception) Exc() *Exception

Return itself.

func (*Exception) Join

func (e *Exception) Join(exc *Exception) *Exception

Join this exception with another exception.

func (*Exception) String

func (e *Exception) String() string

Format an error string from the list of *Error.

type Throwable

type Throwable interface {
	Exc() *Exception
	Error() string
	Chain(e *Error) Throwable
}

The interface that represents what can be thrown (and caught) by this library. All Throwables must be convertible to and *Exception, implement the error interface and allow chaining on of extra errors so that they can be Rethrown.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL