Skip to content

Commit

Permalink
Caution package (#376)
Browse files Browse the repository at this point in the history
* add caution lib

* refactor and add documentation
  • Loading branch information
facuMH authored Dec 16, 2024
1 parent 1c1288e commit 4846231
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 0 deletions.
69 changes: 69 additions & 0 deletions utils/caution/caution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// package caution provides utility functions for handling errors in defer
// cleanup and closing resources.
package caution

import (
"errors"
"fmt"
"io"
)

// ExecuteAndReportError is intended for use in production code to handle errors
// ignored in defer clean ups.
//
// - The first argument is the error variable where the error, if any, will be
// accumulated. If it already contains an error, the new error will be combined.
// - The second argument is a cleanup function that may return an error. This
// function will always be executed.
// - The third argument is a mandatory context message that will be used in the
// error if the cleanup function fails.
//
// Usage example:
//
// Original code:
//
// func F(....) error {
// [...]
// defer f.CleanUpThatMayFail(someArg)
// [...]
// }
//
// Refactored with the new functions:
//
// func F(....) (err error) {
// [...]
// defer ExecuteAndReportError(&err, f() error {f.CleanUpThatMayFail(someArg) }, "failed to cleanup f")
// [...]
// }
func ExecuteAndReportError(err *error, f func() error, message string) {
fErr := f()
if fErr != nil {
*err = errors.Join(*err, fmt.Errorf("%s: %w", message, fErr))
}
}

// CloseAndReportError is specialization of ExecuteAndReportError for types that
// implement the Closer interface, to add error management in the
// `defer f.Close()` pattern.
// Usage example:
//
// Original code:
//
// func F(....) error {
// [...]
// defer f.Close()
// [...]
// }
//
// Refactored with the new functions:
//
// func F(....) (err error) {
// [...]
// defer CloseAndReportError(&err, f, "failed to close f")
// [...]
// }
func CloseAndReportError(err *error, closer io.Closer, message string) {
ExecuteAndReportError(err, func() error {
return closer.Close()
}, message)
}
68 changes: 68 additions & 0 deletions utils/caution/caution_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package caution

import (
"fmt"
"testing"

"github.com/stretchr/testify/require"
)

func TestExecuteAndReportError_ExecutesAndReturnsError(t *testing.T) {
var err error
ExecuteAndReportError(&err, func() error {
return nil
}, "message")
require.NoError(t, err)
someError := fmt.Errorf("someError")
ExecuteAndReportError(&err, func() error {
return someError
}, "message")
require.ErrorIs(t, err, someError)
}

func TestExecuteAndReportError_ExecutesAndCombinesErrors(t *testing.T) {
firstError := fmt.Errorf("firstError")
err := firstError
ExecuteAndReportError(&err, func() error {
return nil
}, "message")
require.ErrorIs(t, err, firstError)

ExecuteAndReportError(&err, func() error {
return fmt.Errorf("secondError")
}, "message")
require.ErrorContains(t, err, "firstError")
require.ErrorContains(t, err, "secondError")
}

type closeMe struct {
err error
}

func (c *closeMe) Close() error {
return c.err
}

func TestCloseAndReportError_(t *testing.T) {
file := &closeMe{}
var err error
CloseAndReportError(&err, file, "message")
require.NoError(t, err)

file.err = fmt.Errorf("someError")
CloseAndReportError(&err, file, "message")
require.ErrorContains(t, err, "message: someError")
}

func TestCloseAndReportError_UsagePatternPropagatesError(t *testing.T) {
expectedError := fmt.Errorf("someError")

testFun := func() (outErr error) {
file := &closeMe{err: expectedError}
defer CloseAndReportError(&outErr, file, "message")
return
}

gotError := testFun()
require.ErrorIs(t, gotError, expectedError)
}

0 comments on commit 4846231

Please sign in to comment.