Terraform
Returning Errors and Warnings
Providers use Diagnostics
to surface errors and warnings to practitioners,
such as contextual messages returned from Terraform CLI at the end of
command output:
$ terraform plan
# ... other plan output ...
╷
│ Error: Summary
│
│ on example.tf line #:
│ #: source configuration line
│
│ Details
╵
In the framework, you may encounter them in response structs or as returns from functions or methods:
func (m myResource) Create(ctx context.Context,
req resource.CreateRequest, resp *resource.CreateResponse)
This is the most common form for Diagnostics: a slice that has one or more errors appended to it. This approach allows your provider to inform practitioners about all relevant errors and warnings at the same time, allowing practitioners to fix their configuration or environment more quickly. You should only append to Diagnostics slices and never replace or remove information from them.
The next section will detail the concepts and typical behaviors of
diagnostics, while the final section will outline the typical methods for
working with diagnostics, using functionality from the available
diag
package.
Diagnostic Concepts
Severity
Severity
specifies whether the diagnostic is an error or a warning. Neither Terraform, nor the framework, supports other severity levels. Use logging for debugging or informational purposes.
- An error will be displayed to the practitioner and halt Terraform's execution, not continuing to apply changes to later resources in the graph. We recommend using errors to inform practitioners about a situation the provider could not recover from.
- A warning will be displayed to the practitioner, but will not halt further execution, and is considered informative only. We recommend using warnings to inform practitioners about suboptimal situations that the practitioner should resolve to ensure stable functioning (e.g., deprecations) or to inform practitioners about possible unexpected behaviors.
Summary
Summary
is a short, practitioner-oriented description of the problem. Good
summaries are general—they don't contain specific details about
values—and concise. For example, "Error creating resource", "Invalid
value for foo", or "Field foo is deprecated".
Detail
Detail
is a longer, more specific practitioner-oriented description of
precisely what went wrong. Good details are specific—they tell the
practitioner exactly what they need to fix and how. For example, "The API
is currently unavailable, please try the request again.", "foo can only contain
letters, numbers, and digits.", or "foo has been deprecated in favor of bar.
Please update your configuration to use bar instead. foo will be removed in a
future release.".
Attribute
Attribute
identifies the specific part of a configuration that caused the
error or warning. Only diagnostics that pertain to a whole attribute or a
specific attribute value will include this information.
Argument
Argument
identifies the specific function argument position that caused the
error or warning. Only diagnostics that pertain to a function argument will
include this information.
How Errors Affect State
Returning an error diagnostic does not stop the state from being updated. Terraform will still persist the returned state even when an error diagnostic is returned with it. This is to allow Terraform to persist the values that have already been modified when a resource modification requires multiple API requests or an API request fails after an earlier one succeeded.
When returning error diagnostics, we recommend resetting the state in the response to the prior state available in the configuration.
diag Package
The framework provides the diag
package for interacting with diagnostics.
While the Go documentation
contains the complete functionality, this section will highlight the most
common methods.
Working With Existing Diagnostics
Append
When receiving diag.Diagnostics
from a function or method, such as
Config.Get()
or State.Set()
, these should typically be appended to the
response diagnostics for the method. This can be accomplished with the
Append(in ...diag.Diagnostics)
method.
For example:
func (m myResource) Create(ctx context.Context,
req resource.CreateRequest, resp *resource.CreateResponse) {
// ... prior logic ...
diags := req.Config.Get(ctx, &resourceData)
resp.Diagnostics.Append(diags...)
// ... further logic ...
}
This method automatically ignores nil
or empty slice diagnostics and
deduplicates where possible.
HasError
The most typical form of diagnostics checking is ensuring that execution should
not stop due to encountering an error, potentially causing further confusing
errors or crashes. The HasError()
method
will check each of the diagnostics for error severity and return true if found.
For example:
func (m myResource) Create(ctx context.Context,
req resource.CreateRequest, resp *resource.CreateResponse) {
// ... prior logic ...
diags := req.Config.Get(ctx, &resourceData)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// ... further logic ...
}
In this example, you will note that we opted to check resp.Diagnostics
instead of diags
. Technically checking either is correct, however, checking
the response diagnostics can help ensure that any response will include the
expected diagnostics.
Creating Diagnostics
When working with logic outside the framework, such as interacting with the
vendor or net/http
library to make the actual calls to manage infrastructure
or creating custom plan modifiers and validators, it will be necessary to
create diagnostics. The diag
package provides helper methods and allows
custom abstractions as described below.
To craft the summary of a diagnostic, it is recommended to use a concise title or single sentence that immediately can allow the practitioner to determine the error cause and when it occurred.
To craft the details portion of diagnostics, it is recommended to provide practitioners (and potentially you as the maintainer) as much contextual, troubleshooting, and next action information as possible. These details can use newlines for easier readability where necessary.
For example, with the top line as a summary and below as details:
API Error Reading Resource
An unexpected error was encountered while reading the resource.
Please check that the credentials being used are active and have sufficient
permissions to perform the Example API call against the resource.
Region: example
ID: example123
API Response: 403 Access Denied
AddError and AddWarning
When creating diagnostics that affect an entire data source, provider, or
resource, and where a diag.Diagnostics
is already available such as within
a response type, the AddError(summary string, detail string)
method
and AddWarning(summary string, detail string)
method
can append a new error or warning diagnostic.
For example:
func (m myResource) Create(ctx context.Context,
req resource.CreateRequest, resp *resource.CreateResponse) {
// ... prior logic ...
resp, err := http.Post("https://example.com")
if err != nil {
resp.Diagnostics.AddError(
"API Error Creating Resource",
fmt.Sprintf("... details ... %s", err)
)
return
}
// ... further logic ...
}
AddAttributeError and AddAttributeWarning
When creating diagnostics that affect only a single attribute, which is
typical of attribute-level plan modifiers and validators, the
AddAttributeError(path path.Path, summary string, detail string)
method
and AddAttributeWarning(path path.Path, summary string, detail string)
method
can append a new error or warning diagnostic pointing specifically at the
attribute path. This provides additional context to practitioners, such as
showing the specific line(s) and value(s) of configuration where possible.
For example:
func (s exampleType) Validate(ctx context.Context, in tftypes.Value, path path.Path) diag.Diagnostics {
var diags diag.Diagnostics
if !in.Type().Is(tftypes.Set{}) {
err := fmt.Errorf()
diags.AddAttributeError(
path,
"Example Type Validation Error",
"An unexpected error was encountered trying to validate an attribute value. "+
"This is always an error in the provider. "+
"Please report the following to the provider developer:\n\n"+
fmt.Sprintf("Expected Set value, received %T with value: %v", in, in),
)
return diags
}
// ... further logic ...
Consistent Diagnostic Creation
Create a helper function in your provider code using the diagnostic creation functions available in the diag
package to generate consistent diagnostics for types of errors/warnings. It is also possible to use custom diagnostics types to accomplish this same goal.
The diag
package provides these functions to create various diagnostics:
Function | Description |
---|---|
diag.NewArgumentErrorDiagnostic() | Create a new error diagnostic with a function argument position. |
diag.NewArgumentWarningDiagnostic() | Create a new warning diagnostic with a function argument position. |
diag.NewAttributeErrorDiagnostic() | Create a new error diagnostic with a path. |
diag.NewAttributeWarningDiagnostic() | Create a new warning diagnostic with a path. |
diag.NewErrorDiagnostic() | Create a new error diagnostic without a path. |
diag.NewWarningDiagnostic() | Create a new warning diagnostic without a path. |
In this example, the provider code is setup to always convert error
returns from the API SDK to a consistent error diagnostic.
func APIErrorDiagnostic(err error) diag.Diagnostic {
return diag.NewErrorDiagnostic(
"Unexpected API Error",
"While calling the API, an unexpected error was returned in the response. "+
"Please contact support if you are unsure how to resolve the error.\n\n"+
"Error: "+err.Error(),
)
}
This enables calling code in the provider, such as:
func (r ThingResource) Read(ctx context.Context, req resource.ReadRequest, resp resource.ReadResponse) {
// ... other logic ...
apiResp, err := examplesdk.Read(/* ... */) // example API SDK call that may return an error
if err != nil {
resp.Diagnostics.Append(APIErrorDiagnostic(err))
return
}
// ... further logic ...
}
Custom Diagnostics Types
Advanced provider developers may want to store additional data in diagnostics for other logic or create custom diagnostics that include specialized logic.
The diag.Diagnostic
interface that can be implemented with these methods:
type Diagnostic interface {
Severity() Severity
Summary() string
Detail() string
Equal(Diagnostic) bool
}
To include attribute path information, the diag.DiagnosticWithPath
interface can be implemented with the additional Path()
method:
type DiagnosticWithPath interface {
Diagnostic
Path() path.Path
}
To include function argument information, the diag.DiagnosticWithFunctionArgument
interface can be implemented with the additional FunctionArgument()
method:
type DiagnosticWithFunctionArgument interface {
Diagnostic
FunctionArgument() int
}
In this example, a custom diagnostic type stores an underlying error
that caused the diagnostic:
// UnderlyingErrorDiagnostic is an error diagnostic
// which also stores the underlying error.
type UnderlyingErrorDiagnostic struct {
Detail string
Summary string
UnderlyingError error
}
func (d UnderlyingErrorDiagnostic) Detail() string {
return d.Detail
}
func (d UnderlyingErrorDiagnostic) Equal(o SpecialDiagnostic) bool {
if d.Detail() != o.Detail() {
return false
}
if d.Summary() != o.Summary() {
return false
}
if d.UnderlyingError == nil {
return o.UnderlyingError == nil
}
if o.UnderlyingError == nil {
return false
}
if d.UnderlyingError.Error() != o.UnderlyingError.Error() {
return false
}
return true
}
func (d UnderlyingErrorDiagnostic) Severity() diag.Severity {
return diag.SeverityError
}
func (d UnderlyingErrorDiagnostic) Summary() string {
return d.Summary
}