Socket
Socket
Sign inDemoInstall

github.com/serhiy-t/errf

Package Overview
Dependencies
1
Alerts
File Explorer

Install Socket

Detect and block malicious and high-risk dependencies

Install

    github.com/serhiy-t/errf

Package errf provides declarative error handling for Go. Each use of errorflow has a scope of a single function. If function uses errorflow its first statement should be IfError() handler configuration: Default configuration is to return first error and never log errors. IfError() handler should always be terminated by one of Then*() functions. Then, when function has IfError() handler setup, all error checks should be done via one of Check* functions: When any of Check* functions encounters an error, it immediately sends a program flow control to IfError() handler, unwinding stack of already registered 'defers'. Internally it uses panic to accomplish this goal. Never use Check* functions in functions without IfError() handler set up (including nested anonymous functions). When running Go tests, errorflow automatically verifies correctness of errorflow usage and panics with message "errflow incorrect call sequence" when the issue is detected. Stacktrace will point to the location of the issue. Validation automatically checks for these rules: By default, validation is only enabled in tests and is disabled in production binaries. To enable/disable validation (e.g. because of performance issues, false positives, incorrect test/prod environment detection), clients can use SetNoopValidator or SetStackTraceValidator functions. Return strategy controls what error to return from a function in case if multiple errors were produced (e.g. by gzipWriter.Close() function and then fileWriter.Close() function). Return strategy is set by using one of Return* functions from IfError() handler. Available strategies: Example: Log strategy controls IfError() handler logging behavior. Available strategies: Example: Wrappers are functions which wrap error objects into other error objects. They can be applied to: Example: Custom wrappers can be implemented using errf.Wrapper function: Usage: Handlers are used to handle errors and panics that are bubbling up the defers ladder. Unlike IfError()... handler, Handle() API can be used in a middle of the function, thus e.g. skipped if preparation steps haven't been executed yet. This might be useful for additional cleanup logic. Consider function that copies files. If there is an error during copying, we would want to delete destination file rather than leaving it in inconsistent state. But, unlike closing a file, this action needs to be performed only when there's an error and should not be performed on a successful copy. Here's how handlers can be used to implement this logic: For full list of handlers, see documentation for: Notes: Custom log function can be set using SetLogFn method: Errflow also implements few helper functions which can be used in functions without IfError() handler. This functions might be configured using errf.With(...). NOTE: since these functions don't use IfError() handler, they will not use config defined on IfError() handler. Library clients can create new Check* functions for custom return types. This is useful for writing type safe code (until generics will solve this problem). Example:


Version published

Readme

Source

Build Status Go Report Card Documentation

ErrorFlow

Declarative error handling for Go.

Motivation

Reading list:

ErrorFlow goal is to provide a library solution to the issues raised in articles above.

Library solution (as opposed to a language change), although less clean, has a very important benefit: it is optional. Many language proposals for addressing this issue have been rejected because language change is required to be universally applicable. Library solution can be used only for use cases where it works well.

Features

  • 'err'-variable-free type-safe branchless business logic
reader := errf.Io.CheckReadCloser(os.Open(srcFilename))
	/* vs */
reader, err := os.Open(srcFilename)
if err != nil {
	return err
}
  • Declarative multiple errors handling logic
defer errf.IfError().ReturnFirst().ThenAssignTo(&err)
defer errf.CheckDeferErr(writer.Close)
	/* vs */
defer func() {
	closeErr := writer.Close()
	if closeErr != nil and err == nil {
		err = closeErr
	}
}()
  • Declarative errors logging logic
defer errf.IfError().LogIfSuppressed().ThenAssignTo(&err)
defer errf.CheckDeferErr(writer.Close)
	/* vs */
defer func() {
	closeErr := writer.Close()
	if closeErr != nil {
		if err == nil {
			err = closeErr
		} else {
			log.Printf("error closing writer: %w", err)
		}
	}
}()
  • Doesn't affect APIs
    • Every use of ErrorFlow is scoped to a single function and doesn't leak into its API
  • Extendable
    • Custom return types for type safety
    • Custom ErrorFlow config functions (e.g. creating a wrapper that converts errors from a third-party libraries into standard error types for an internal codebase)

Example: error handling for a file gzip function

Error handling requirements for function:

  • Returns error only in case of error that affects result file correctness.
  • Cleans up dst file in case of the error instead of leaving it in inconsistent state.
  • Logs all internal errors that it didn't return.
  • Wraps returned errors with "error compressing file: " prefix.
  • Performs input parameters validation.

ErrorFlow style error handling

func GzipFile(dstFilename string, srcFilename string) (err error) {
	// defer IfError()... creates and configures
	// ErrorFlow error handler for this function.
	// When any of Check* functions encounters non-nil error
	// it immediately sends error to this handler
	// unwinding all stacked defers.
	errWrapper := errf.WrapperFmtErrorw("error compressing file")
	defer errf.IfError().ReturnFirst().LogIfSuppressed().Apply(errWrapper).ThenAssignTo(&err)

	errf.CheckAssert(len(dstFilename) > 0, "dst file should be specified")
	errf.CheckAssert(len(srcFilename) > 0, "src file should be specified")

	reader := errf.Io.CheckReadCloser(os.Open(srcFilename))
	defer errf.With(errWrapper).LogDefer(reader.Close)

	writer := errf.Io.CheckWriteCloser(os.Create(dstFilename))
	defer errf.Handle().OnAnyErrOrPanic(func() { os.Remove(dstFilename) })
	defer errf.CheckDeferErr(writer.Close)

	gzipWriter := gzip.NewWriter(writer)
	defer errf.CheckDeferErr(gzipWriter.Close)

	return errf.CheckDiscard(io.Copy(gzipWriter, reader)).IfOkReturnNil
}

Compare with

Plain Go implementation without any error handling
func GzipFile(dstFilename string, srcFilename string) error {
	reader, _ := os.Open(srcFilename)
	defer reader.Close()

	writer, _ := os.Create(dstFilename)
	defer writer.Close()

	gzipWriter := gzip.NewWriter(writer)
	defer gzipWriter.Close()

	_, _ = io.Copy(gzipWriter, reader)

	return nil
}
Plain Go implementation (functionally roughly equivalent to ErrorFlow example above; not using helper functions)
func GzipFile(dstFilename string, srcFilename string) (err error) {
	if len(dstFilename) == 0 {
		return fmt.Errorf("error compressing file: dst file should be specified")
	}
	if len(srcFilename) == 0 {
		return fmt.Errorf("error compressing file: src file should be specified")
	}

	reader, err := os.Open(srcFilename)
	if err != nil {
		return fmt.Errorf("error compressing file: %w", err)
	}
	defer func() {
		closeErr := reader.Close()
		if closeErr != nil {
			log.Println(closeErr)
		}
	}()

	writer, err := os.Create(dstFilename)
	if err != nil {
		return fmt.Errorf("error compressing file: %w", err)
	}
	defer func() {
		if err != nil {
			os.Remove(dstFilename)
		}
	}()
	defer func() {
		closeErr := writer.Close()
		if closeErr != nil {
			if err == nil {
				err = fmt.Errorf("error compressing file: %w", closeErr)
			} else {
				log.Println(fmt.Errorf("[suppressed] error compressing file: %w", closeErr))
			}
		}
	}()

	gzipWriter := gzip.NewWriter(writer)
	defer func() {
		closeErr := gzipWriter.Close()
		if closeErr != nil {
			if err == nil {
				err = fmt.Errorf("error compressing file: %w", closeErr)
			} else {
				log.Println(fmt.Errorf("[suppressed] error compressing file: %w", closeErr))
			}
		}
	}()

	_, err = io.Copy(gzipWriter, reader)
	if err != nil {
		return fmt.Errorf("error compressing file: %w", err)
	}

	return nil
}
ErrorFlow-Lite style error handling (using only defer helper functions, but not IfError/Check* handler)
func GzipFile(dstFilename string, srcFilename string) (err error) {
	errflow := errf.With(
		errf.LogStrategyIfSuppressed,
		errf.WrapperFmtErrorw("error compressing file"),
	)

	if len(dstFilename) == 0 {
		return fmt.Errorf("error compressing file: dst file should be specified")
	}
	if len(srcFilename) == 0 {
		return fmt.Errorf("error compressing file: src file should be specified")
	}

	reader, err := os.Open(srcFilename)
	if err != nil {
		return fmt.Errorf("error compressing file: %w", err)
	}
	defer errflow.LogDefer(reader.Close)

	writer, err := os.Create(dstFilename)
	if err != nil {
		return fmt.Errorf("error compressing file: %w", err)
	}
	defer func() {
		if err != nil {
			os.Remove(dstFilename)
		}
	}()
	defer errflow.IfErrorAssignTo(&err, writer.Close)

	gzipWriter := gzip.NewWriter(writer)
	defer errflow.IfErrorAssignTo(&err, gzipWriter.Close)

	_, err = io.Copy(gzipWriter, reader)
	if err != nil {
		return fmt.Errorf("error compressing file: %w", err)
	}

	return nil
}

FAQs

Last updated on 28 Mar 2021

Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc