Repository: uber-go/fx
Pull request #1002 (2022-12-21 19:30:29)

Panic Recovery Option
Since https://github.com/uber-go/dig/pull/364 was merged into Dig, we can now built upon it to add the ability to recover from panics in user-provided code into Fx, by providing a `fx.RecoverFromPanics()` option into their application. This is disallowed on the module level, only on the application level.

```go
app := fx.New(
    fx.Provide(func() int {
        panic("terrible sorrow")
    }),
    fx.Invoke(func(a int) {}),
    fx.RecoverFromPanics(), // panic will be set to app.Err now
)

err := app.Err()
var pe dig.PanicError
if errors.As(err, &pe) { // panic happened when we invoke
    // handle panic
}
```

codecov[bot]: # [Codecov](https://codecov.io/gh/uber-go/fx/pull/1002?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) Report
> Merging [#1002](https://codecov.io/gh/uber-go/fx/pull/1002?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (64fd4dd) into [master](https://codecov.io/gh/uber-go/fx/commit/6d76d64292ae1f5ef8954a0c1875abb3078d9cb7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (6d76d64) will **increase** coverage by `0.07%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master    #1002      +/-   ##
==========================================
+ Coverage   98.31%   98.39%   +0.07%     
==========================================
  Files          39       33       -6     
  Lines        2017     1926      -91     
==========================================
- Hits         1983     1895      -88     
+ Misses         26       24       -2     
+ Partials        8        7       -1     
```


| [Impacted Files](https://codecov.io/gh/uber-go/fx/pull/1002?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) | Coverage Δ | |
|---|---|---|
| [app.go](https://codecov.io/gh/uber-go/fx/pull/1002/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-YXBwLmdv) | `96.55% <100.00%> (+0.14%)` | :arrow_up: |
| [docs/internal/httptest/http.go](https://codecov.io/gh/uber-go/fx/pull/1002/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC9odHRwdGVzdC9odHRwLmdv) | | |
| [docs/internal/iotest/read.go](https://codecov.io/gh/uber-go/fx/pull/1002/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC9pb3Rlc3QvcmVhZC5nbw==) | | |
| [docs/internal/exectest/cmd.go](https://codecov.io/gh/uber-go/fx/pull/1002/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC9leGVjdGVzdC9jbWQuZ28=) | | |
| [docs/internal/apptest/run.go](https://codecov.io/gh/uber-go/fx/pull/1002/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC9hcHB0ZXN0L3J1bi5nbw==) | | |
| [docs/internal/test/fake.go](https://codecov.io/gh/uber-go/fx/pull/1002/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC90ZXN0L2Zha2UuZ28=) | | |
| [docs/internal/exectest/output.go](https://codecov.io/gh/uber-go/fx/pull/1002/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC9leGVjdGVzdC9vdXRwdXQuZ28=) | | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)


Patch:
diff --git a/app.go b/app.go
index 0ff5c10..b0452b8 100644
--- a/app.go
+++ b/app.go
@@ -165,6 +165,28 @@ func (t stopTimeoutOption) String() string {
 	return fmt.Sprintf("fx.StopTimeout(%v)", time.Duration(t))
 }
 
+// RecoverFromPanics causes panics that occur in functions given to [Provide],
+// [Decorate], and [Invoke] to be recovered from.
+// This error can be retrieved as any other error, by using (*App).Err().
+func RecoverFromPanics() Option {
+	return recoverFromPanicsOption{}
+}
+
+type recoverFromPanicsOption struct{}
+
+func (o recoverFromPanicsOption) apply(m *module) {
+	if m.parent != nil {
+		m.app.err = fmt.Errorf("fx.RecoverFromPanics Option should be passed to top-level " +
+			"App, not to fx.Module")
+	} else {
+		m.app.recoverFromPanics = true
+	}
+}
+
+func (o recoverFromPanicsOption) String() string {
+	return "fx.RecoverFromPanics()"
+}
+
 // WithLogger specifies how Fx should build an fxevent.Logger to log its events
 // to. The argument must be a constructor with one of the following return
 // types.
@@ -284,6 +306,8 @@ type App struct {
 	// Decides how we react to errors when building the graph.
 	errorHooks []ErrorHandler
 	validate   bool
+	// Whether to recover from panics in Dig container
+	recoverFromPanics bool
 
 	// Used to signal shutdowns.
 	receivers signalReceivers
@@ -430,10 +454,16 @@ func New(opts ...Option) *App {
 		lifecycle.New(appLogger{app}, app.clock),
 	}
 
-	app.container = dig.New(
+	containerOptions := []dig.Option{
 		dig.DeferAcyclicVerification(),
 		dig.DryRun(app.validate),
-	)
+	}
+
+	if app.recoverFromPanics {
+		containerOptions = append(containerOptions, dig.RecoverFromPanics())
+	}
+
+	app.container = dig.New(containerOptions...)
 
 	for _, m := range app.modules {
 		m.build(app, app.container)
diff --git a/docs/go.mod b/docs/go.mod
index 0c98d32..32a2d4f 100644
--- a/docs/go.mod
+++ b/docs/go.mod
@@ -19,3 +19,5 @@ require (
 )
 
 replace go.uber.org/fx => ../
+
+replace go.uber.org/dig => github.com/uber-go/dig v1.15.1-0.20221213190814-6ef87c8bcb68
diff --git a/docs/go.sum b/docs/go.sum
index 5592d30..6afebe5 100644
--- a/docs/go.sum
+++ b/docs/go.sum
@@ -11,10 +11,10 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/uber-go/dig v1.15.1-0.20221213190814-6ef87c8bcb68 h1:lCRB/UCzzaj+o5obu4qVC3MuInDt7g2mDVSJvHWSKYc=
+github.com/uber-go/dig v1.15.1-0.20221213190814-6ef87c8bcb68/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk=
 go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/dig v1.15.0 h1:vq3YWr8zRj1eFGC7Gvf907hE0eRjPTZ1d3xHadD6liE=
-go.uber.org/dig v1.15.0/go.mod h1:pKHs0wMynzL6brANhB2hLMro+zalv1osARTviTcqHLM=
 go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
 go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
diff --git a/go.mod b/go.mod
index 13f7f31..c689298 100644
--- a/go.mod
+++ b/go.mod
@@ -18,3 +18,5 @@ require (
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
+
+replace go.uber.org/dig => github.com/uber-go/dig v1.15.1-0.20221213190814-6ef87c8bcb68
diff --git a/go.sum b/go.sum
index 2c75e34..a79f1b4 100644
--- a/go.sum
+++ b/go.sum
@@ -18,11 +18,11 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/uber-go/dig v1.15.1-0.20221213190814-6ef87c8bcb68 h1:lCRB/UCzzaj+o5obu4qVC3MuInDt7g2mDVSJvHWSKYc=
+github.com/uber-go/dig v1.15.1-0.20221213190814-6ef87c8bcb68/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/dig v1.15.0 h1:vq3YWr8zRj1eFGC7Gvf907hE0eRjPTZ1d3xHadD6liE=
-go.uber.org/dig v1.15.0/go.mod h1:pKHs0wMynzL6brANhB2hLMro+zalv1osARTviTcqHLM=
 go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
 go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
 go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=


Test patch:
diff --git a/app_test.go b/app_test.go
index e225786..d07c1ad 100644
--- a/app_test.go
+++ b/app_test.go
@@ -919,6 +919,43 @@ func TestTimeoutOptions(t *testing.T) {
 	assert.True(t, stopped, "app wasn't stopped")
 }
 
+func TestRecoverFromPanicsOption(t *testing.T) {
+	t.Parallel()
+
+	t.Run("CannotGiveToModule", func(t *testing.T) {
+		t.Parallel()
+		mod := Module("MyModule", RecoverFromPanics())
+		err := New(mod).Err()
+		require.Error(t, err)
+		require.Contains(t, err.Error(),
+			"fx.RecoverFromPanics Option should be passed to top-level App, "+
+				"not to fx.Module")
+	})
+
+	run := func(withOption bool) {
+		opts := []Option{
+			Provide(func() int {
+				panic("terrible sorrow")
+			}),
+			Invoke(func(a int) {}),
+		}
+		if withOption {
+			opts = append(opts, RecoverFromPanics())
+			app := New(opts...)
+			err := app.Err()
+			require.Error(t, err)
+			assert.Contains(t, err.Error(),
+				`panic: "terrible sorrow" in func: "go.uber.org/fx_test".TestRecoverFromPanicsOption.`)
+		} else {
+			assert.Panics(t, func() { New(opts...) },
+				"expected panic without RecoverFromPanics() option")
+		}
+	}
+
+	t.Run("WithoutOption", func(t *testing.T) { run(false) })
+	t.Run("WithOption", func(t *testing.T) { run(true) })
+}
+
 func TestAppRunTimeout(t *testing.T) {
 	t.Parallel()
 
@@ -2146,6 +2183,11 @@ func TestOptionString(t *testing.T) {
 			give: StopTimeout(5 * time.Second),
 			want: "fx.StopTimeout(5s)",
 		},
+		{
+			desc: "RecoverFromPanics",
+			give: RecoverFromPanics(),
+			want: "fx.RecoverFromPanics()",
+		},
 		{
 			desc: "Logger",
 			give: WithLogger(func() fxevent.Logger { return testLogger{t} }),