BaseApp.runTx() function handles Go panics that might occur during transactions execution, for example, keeper has faced an invalid state and paniced. Depending on the panic type different handler is used, for instance the default one prints an error log message. Recovery middleware is used to add custom panic recovery for Cosmos SDK application developers. More context can found in the corresponding ADR-022 and the implementation in recovery.go.

Interface

package baseapp

import (
    
	"fmt"
    "runtime/debug"

	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

/ RecoveryHandler handles recovery()

object.
/ Return a non-nil error if recoveryObj was processed.
/ Return nil if recoveryObj was not processed.
type RecoveryHandler func(recoveryObj interface{
})

error

/ recoveryMiddleware is wrapper for RecoveryHandler to create chained recovery handling.
/ returns (recoveryMiddleware, nil)
    if recoveryObj was not processed and should be passed to the next middleware in chain.
/ returns (nil, error)
    if recoveryObj was processed and middleware chain processing should be stopped.
type recoveryMiddleware func(recoveryObj interface{
}) (recoveryMiddleware, error)

/ processRecovery processes recoveryMiddleware chain for recovery()

object.
/ Chain processing stops on non-nil error or when chain is processed.
func processRecovery(recoveryObj interface{
}, middleware recoveryMiddleware)

error {
    if middleware == nil {
    return nil
}

next, err := middleware(recoveryObj)
    if err != nil {
    return err
}

return processRecovery(recoveryObj, next)
}

/ newRecoveryMiddleware creates a RecoveryHandler middleware.
func newRecoveryMiddleware(handler RecoveryHandler, next recoveryMiddleware)

recoveryMiddleware {
    return func(recoveryObj interface{
}) (recoveryMiddleware, error) {
    if err := handler(recoveryObj); err != nil {
    return nil, err
}

return next, nil
}
}

/ newOutOfGasRecoveryMiddleware creates a standard OutOfGas recovery middleware for app.runTx method.
func newOutOfGasRecoveryMiddleware(gasWanted uint64, ctx sdk.Context, next recoveryMiddleware)

recoveryMiddleware {
    handler := func(recoveryObj interface{
})

error {
    err, ok := recoveryObj.(sdk.ErrorOutOfGas)
    if !ok {
    return nil
}

return sdkerrors.Wrap(
			sdkerrors.ErrOutOfGas, fmt.Sprintf(
				"out of gas in location: %v; gasWanted: %d, gasUsed: %d",
				err.Descriptor, gasWanted, ctx.GasMeter().GasConsumed(),
			),
		)
}

return newRecoveryMiddleware(handler, next)
}

/ newDefaultRecoveryMiddleware creates a default (last in chain)

recovery middleware for app.runTx method.
func newDefaultRecoveryMiddleware()

recoveryMiddleware {
    handler := func(recoveryObj interface{
})

error {
    return sdkerrors.Wrap(
			sdkerrors.ErrPanic, fmt.Sprintf(
				"recovered: %v\nstack:\n%v", recoveryObj, string(debug.Stack()),
			),
		)
}

return newRecoveryMiddleware(handler, nil)
}
recoveryObj is a return value for recover() function from the buildin Go package. Contract:
  • RecoveryHandler returns nil if recoveryObj wasn’t handled and should be passed to the next recovery middleware;
  • RecoveryHandler returns a non-nil error if recoveryObj was handled;

Custom RecoveryHandler register

BaseApp.AddRunTxRecoveryHandler(handlers ...RecoveryHandler) BaseApp method adds recovery middleware to the default recovery chain.

Example

Lets assume we want to emit the “Consensus failure” chain state if some particular error occurred. We have a module keeper that panics:
func (k FooKeeper)

Do(obj interface{
}) {
    if obj == nil {
        / that shouldn't happen, we need to crash the app
    err := errorsmod.Wrap(fooTypes.InternalError, "obj is nil")

panic(err)
}
}
By default that panic would be recovered and an error message will be printed to log. To override that behaviour we should register a custom RecoveryHandler:
/ Cosmos SDK application constructor
    customHandler := func(recoveryObj interface{
})

error {
    err, ok := recoveryObj.(error)
    if !ok {
    return nil
}
    if fooTypes.InternalError.Is(err) {
    panic(fmt.Errorf("FooKeeper did panic with error: %w", err))
}

return nil
}
    baseApp := baseapp.NewBaseApp(...)

baseApp.AddRunTxRecoveryHandler(customHandler)