Skip to content

docs: Tango Errors RFC - #161

Merged
justinwon777 merged 2 commits into
mainfrom
justin.won/rfc
Jul 15, 2026
Merged

docs: Tango Errors RFC#161
justinwon777 merged 2 commits into
mainfrom
justin.won/rfc

Conversation

@justinwon777

Copy link
Copy Markdown
Contributor

Adds a Tango Errors RFC covering the core/errors package and its APIs

@justinwon777
justinwon777 requested review from a team as code owners July 8, 2026 19:04
Comment thread docs/errors/errors.md Outdated
Comment thread docs/errors/errors.md Outdated
Comment thread docs/errors/errors.md Outdated
Comment thread docs/errors/errors.md Outdated
Comment thread docs/errors/errors.md Outdated
Comment thread docs/errors/errors.md Outdated
@justinwon777
justinwon777 force-pushed the justin.won/rfc branch 5 times, most recently from bca8e66 to 60ac16c Compare July 8, 2026 22:36

@sbalabanov sbalabanov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few things are missing in the RFC design:

  • Clear error domain definition. What types of errors could ever be in the system? (see comments on data structures).
  • Guidelines on using classifiers; where exactly should classification be done and what data will it use. In the provided example, bazel query error may be both infra (i.e. no bazel binary on the machine) or user (i.e. change is incompatible with the trunk). Infra might be retryable (i.e. bazilisk failed with http error) or not (i.e. incorrect argument list to this specific bazel version).
  • How internal classification translates to the API layer (GRPC codes and rich error object). GRPC defines its own codes which cannot be changed or extended, typically observable by common infrastructure (i.e. muttley).
  • Missing dependency flag. We would like to compute inclusive and exclusive reliability metric for the service, i.e. it should differentiate between its own errors (tango process + subprocesses it runs) and external systems it calls (i.e. CI or storage).

Comment thread docs/errors/errors.md Outdated
Comment thread docs/errors/errors.md Outdated
| Error Code | Definition |
|---|---|
| `ERROR_UNKNOWN` | Unexpected errors |
| `ERROR_BAD_REQUEST` | Request validation errors (missing arg, invalid arg), canceled requests, user failures in builds |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to differentiate between missing arg (which is a programming error on a caller side) vs build failure (which is a user error not related to the ecosystem correctness)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree we should eventually have different error codes for different types of user failures like canceled and build failure, but I think we should take advantage of that once our error classification is more mature. For instance, right now there is no way for Tango to know why a buildkite build failed, so there is no use of having a build failure error code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a cancelled error code since we see that one a lot and think it'd be helpful to distinguish cancelled from other user failures.

Comment thread docs/errors/errors.md Outdated
Comment thread docs/errors/errors.md Outdated
Comment thread docs/errors/errors.md Outdated
@justinwon777

justinwon777 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@sbalabanov partial response, still looking into your GRPC codes comment

  • Guidelines on using classifiers; where exactly should classification be done and what data will it use. In the provided example, bazel query error may be both infra (i.e. no bazel binary on the machine) or user (i.e. change is incompatible with the trunk). Infra might be retryable (i.e. bazilisk failed with http error) or not (i.e. incorrect argument list to this specific bazel version).

I added a more detailed example of the usage. This package's purpose is to provide the framework for callers to classify an error, and the caller is responsible for determining how to classify an error. At the controller layer, the mapper will handle all errors and convert them to the proto type (unclassified will be unknown error code). Some packages like bazel will need more work done to differentiate user and infra failures.

  • Missing dependency flag. We would like to compute inclusive and exclusive reliability metric for the service, i.e. it should differentiate between its own errors (tango process + subprocesses it runs) and external systems it calls (i.e. CI or storage).

Yes we could potentially add two more error codes ErrorInternalDependency and ErrorInternalDependencyRetryable. In a future task we can identify external systems that can use these and then add the error codes.

@justinwon777
justinwon777 force-pushed the justin.won/rfc branch 5 times, most recently from 4802d8c to 36d7f4a Compare July 9, 2026 21:49
@justinwon777

justinwon777 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author
  • How internal classification translates to the API layer (GRPC codes and rich error object). GRPC defines its own codes which cannot be changed or extended, typically observable by common infrastructure (i.e. muttley).

Added the gRPC code mapping to the error code definitions

@justinwon777
justinwon777 force-pushed the justin.won/rfc branch 2 times, most recently from 2867fb2 to 4557c91 Compare July 10, 2026 00:23
Comment thread docs/errors/errors.md Outdated
Comment thread docs/errors/errors.md Outdated
Comment thread docs/errors/errors.md
### type TangoError

```go
type TangoError struct {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to implement fmt.Formatter for this type? See https://pkg.go.dev/github.com/pkg/errors#hdr-Formatted_printing_of_errors.

If you impl this, the errors will get a Verbose field populated using the formatter when you log it thru Zap.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering we're going to log errors with the zap fields, the error, failure source, and error code will be in the log, so I don't think having another verbose field would add anything not already in the log.

Comment thread docs/errors/errors.md Outdated
Comment thread docs/errors/errors.md Outdated
Comment thread docs/errors/errors.md Outdated
## Usage

### Wrapping errors at the failure site
Callers should wrap their errors with TangoError using the provided constructors. It is up to the caller to handle classifying an error. In the following example, the bazel package creates a helper func to create a TangoError with `ErrorInfra` classification. This helper is used in `ensureBazelisk` which runs os operations that can fail with infra failures.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

potential counter case
let's say two very independent functions use storage function Get()
let's say storage implementation is mysql, and it returns mysql-specific "connection failure" error code as mysql error object, which is infra_retryable error
will both call sites run an independent classification of such an event? How do they know they have to classify mysql exit codes and cast to mysql error type?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect Get() to classify the error, so the two independent functions receive the classified error. I don't think it should be the caller's responsibility to know how to handle storage errors. Same with when SQ calls Tango it'd be better if Tango returned the classified error than SQ classifying it right? Which is why we're making this errors package.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following our discussion I updated it so component packages will return sentinels and top level layers that call the component will check for the sentinel and classify it.

@justinwon777
justinwon777 force-pushed the justin.won/rfc branch 2 times, most recently from 1e33bc8 to d36d143 Compare July 15, 2026 11:04
Comment thread docs/errors/errors.md
}

// orchestrator
func classifyBazelClientError(err error) error {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little concerned if different layers call the same component then each layer needs to implement classifying the same sentinels. I haven't seen a case yet where Tango would classify the same sentinel differently, so the classifying logic could get redundant. Although I think there aren't that many user/infra-retryable errors in Tango, so it's not like we're checking sentinels on every single error.

Comment thread docs/errors/errors.md Outdated
|---|---|
| `ERROR_INFRA` | Infra failures within Tango APIs not caused by the user |
| `ERROR_CANCELLED` | Client canceled the request |
| `ERROR_USER` | Request validation errors (missing arg, invalid arg) |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not just validation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I only put validation here cause currently in OSS we only have validation errors, but in internal service we will have other possible user failures like build failures. Updated so it doesn't seem like it's only validation failures.

Comment thread docs/errors/errors.md Outdated

```go
// bazel
var ErrDownloadBazelisk = errors.New("download bazelisk")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think the sentinel is a good example as it does not show the reason of the error.
HTTP download can fail because connection to a server fails (likely recoverable) or if the required version does not exist on a server (404) - likely not recoverable.
Here both of them are combined.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc for Do says it can return a network error and non 2xx error codes are not returned in err, but in the response body with nil err. I added a net.Error check to the example.

Comment thread docs/errors/errors.md Outdated
@justinwon777
justinwon777 force-pushed the justin.won/rfc branch 2 times, most recently from 8eb3427 to 174ba8a Compare July 15, 2026 21:00
@justinwon777
justinwon777 merged commit 4ba81a2 into main Jul 15, 2026
13 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants