# What Happens to Memoized Results When the Original Function Returns an Error?

> Discover what happens to memoized results when a function returns an error. Learn how errors are cached, preventing re-execution and ensuring consistent error returns.

- Repository: [Aperture Robotics/util](https://github.com/aperturerobotics/util)
- Tags: deep-dive
- Published: 2026-02-25

---

**Memoized functions permanently cache errors just like successful results, returning the same error on every subsequent call without re-executing the original function.**

When using the `MemoizeFunc` utility from the `aperturerobotics/util` repository, understanding how errors are handled is critical for production reliability. The memoization mechanism in [`memo/memo.go`](https://github.com/aperturerobotics/util/blob/main/memo/memo.go) stores both return values and errors from the original function, meaning that a single failure becomes the permanent result for all future invocations. This article examines the source code implementation to explain exactly how errors are captured and why they are never retried.

## How MemoizeFunc Caches Errors

The `MemoizeFunc` implementation uses a closure to store the results of the first execution. According to the source code in [`memo/memo.go`](https://github.com/aperturerobotics/util/blob/main/memo/memo.go), the function declares private variables `result` and `doneErr` that capture whatever the wrapped function returns—whether a valid value, an error, or both.

The implementation follows this pattern:

```go
func MemoizeFunc[T any](fn func() (T, error)) func() (T, error) {
    var started atomic.Bool
    done := make(chan struct{})
    var result T
    var doneErr error
    return func() (T, error) {
        if !started.Swap(true) {          // first call
            defer close(done)             // signal completion
            result, doneErr = fn()        // run original function once
            return result, doneErr
        }
        <-done                            // wait for first call to finish
        return result, doneErr            // return cached values (including error)
    }
}

```

Key observations from the `aperturerobotics/util` source:

- **First execution**: The original function `fn()` runs exactly once, and both its return value and error are stored in `result` and `doneErr`.
- **Synchronization**: The `done` channel closes immediately after `fn()` completes, unblocking any concurrent callers waiting for the result.
- **Subsequent calls**: All later invocations bypass the original function entirely, returning the cached `result` and `doneErr` directly.

### Permanent Error Storage

Because the closure captures the error value in `doneErr` during the initial execution, that error becomes immutable for the lifetime of the memoized function. The `MemoizeFunc` does not distinguish between successful results and failures—it caches both with identical persistence. Consequently, if the original function returns an error, that error is **not retried**; it becomes the permanent result of the memoized function.

## Practical Example: Memoizing a Flaky Function

Consider a function that fails deterministically. The following example demonstrates how `MemoizeFunc` handles the error:

```go
package main

import (
	"errors"
	"fmt"
	"time"
	
	"github.com/aperturerobotics/util/memo"
)

// Simulate a function that always fails
func flaky() (int, error) {
	fmt.Println("flaky() invoked")
	return 0, errors.New("temporary failure")
}

func main() {
	memoized := memo.MemoizeFunc(flaky)

	// First call – executes flaky() and caches the error
	_, err := memoized()
	fmt.Println("first call error:", err) // => temporary failure

	// Second call – returns cached error without executing flaky()
	_, err = memoized()
	fmt.Println("second call error:", err) // => temporary failure

	// Even after waiting, the error remains cached
	time.Sleep(100 * time.Millisecond)
	_, err = memoized()
	fmt.Println("after wait error:", err) // => temporary failure
}

```

Output:

```

flaky() invoked
first call error: temporary failure
second call error: temporary failure
after wait error: temporary failure

```

Note that `flaky()` prints exactly once, confirming that the original function never re-executes despite the error. The unit tests in [`memo/memo_test.go`](https://github.com/aperturerobotics/util/blob/main/memo/memo_test.go) verify the caching behavior for successful results, and the same mechanism applies identically to error returns.

## Summary

- **Errors are cached permanently**: `MemoizeFunc` stores the error from the first execution in the closure's `doneErr` variable.
- **No retry mechanism**: The original function executes exactly once regardless of whether it returns an error or a value.
- **Thread-safe propagation**: The `done` channel ensures that all concurrent and subsequent callers receive the same cached error after the first call completes.
- **Source location**: This behavior is implemented in [`memo/memo.go`](https://github.com/aperturerobotics/util/blob/main/memo/memo.go) within the `aperturerobotics/util` repository.

## Frequently Asked Questions

### Does MemoizeFunc retry the original function if it returns an error?

No. `MemoizeFunc` treats errors as valid return values and caches them permanently. Once the original function returns an error, that error is stored in the closure and returned for all subsequent calls without re-executing the function. This design ensures that expensive or side-effect-producing functions run at most once.

### How does MemoizeFunc handle concurrent calls while the first execution is still running?

Concurrent calls block on the `<-done` channel receive operation. Once the first call completes and closes the `done` channel, all waiting goroutines wake up and receive the same cached result and error. This ensures that the original function executes exactly once even under concurrent access, as confirmed by the synchronization logic in [`memo/memo.go`](https://github.com/aperturerobotics/util/blob/main/memo/memo.go).

### Can I clear or reset the memoized error to retry the function?

The current implementation in `aperturerobotics/util` does not provide a reset mechanism. The memoized function maintains its cached state for its entire lifetime. To retry a failed function, you must create a new memoized instance by calling `MemoizeFunc` again with the original function.

### Where is the error caching logic implemented in the source code?

The error caching logic resides in [`memo/memo.go`](https://github.com/aperturerobotics/util/blob/main/memo/memo.go) in the `aperturerobotics/util` repository. The relevant variables are `doneErr` (which stores the error) and `result` (which stores the return value), both captured in the closure returned by `MemoizeFunc`. The test suite in [`memo/memo_test.go`](https://github.com/aperturerobotics/util/blob/main/memo/memo_test.go) validates the caching behavior for various execution scenarios.