Go 1.21 - Cancelamento de Contexto com erro customizado

Quantas vezes você já se deparou com um erro de “contexto cancelado” sem conseguir identificar a origem do erro?

Por exemplo, quando o contexto é cancelado, recebemos um erro do tipo context.Canceled, com uma mensagem de erro padrão. No entanto, em cenários onde várias operações estão ocorrendo, é difícil determinar qual operação foi cancelada.

func Example1() {
	parentCtx := context.Background()
	ctx, cancel := context.WithCancel(parentCtx)

	cancel()

	if err := ctx.Err(); err != nil {
		fmt.Println(err) // context canceled
	}
}

A partir do Go 1.21, foi adicionado a possibilidade de personalizar o esse erro, na verdade, é um sub error, já que o error original continua o mesmo.

Com o WithCancelCause , podemos passar um erro personalizado na hora de fazer o cancelamento do contexto, e assim ter mais informações quando o contexto for cancelado.

var ErrOperationCanceled = errors.New("operation canceled")

func Example2() {
	parentCtx := context.Background()
	ctx, cancel := context.WithCancelCause(parentCtx)

	cancel(ErrOperationCanceled)

	if err := ctx.Err(); err != nil {
		fmt.Println(context.Cause(ctx)) // operation canceled
	}
}

No exemplo acima, passamos um erro personalizado para o cancelamento do contexto, e depois usamos a função Cause para saber qual foi o erro que cancelou o contexto.

É importante destacar que o comportamento do ctx.Err() permanece o mesmo, ele vai retornar context.Canceled ou context.DeadlineExceeded dependendo como foi criado o contexto.

Outras funções também receberam uma versão com a possibilidade de passar um erro personalizado, como: WithDeadlineCause e WithTimeoutCause , com uma pequena diferença, que nessas funções o erro personalizado é passado como argumento, no momento de criar o contexto.

var ErrTimeout = errors.New("timeout error")

func Example3() {
	parentCtx := context.Background()
	ctx, _ := context.WithTimeoutCause(parentCtx, time.Millisecond*100, ErrTimeout)

	// wait for the context to timeout
	<-ctx.Done()

	if ctx.Err() == context.DeadlineExceeded {
		fmt.Println(context.Cause(ctx))
	}
}

Essas são mudanças sutis, mas que irão auxiliar significativamente na rastreabilidade de erros em aplicações que utilizam contextos.

Por hoje é isso, até próxima!