# A robust system to handle and display complex errors

By Brandon Bayer · Feb 15, 2024

Here at Flightcontrol, it feels like errors are our daily nutrition. We are a layer on top of a user’s AWS account that provides delightful automation so users can focus on building product instead of infrastructure.

Our system is bombarded with expected and unexpected errors from all directions: users’ code failing to build or deploy, improper configuration, lots and lots of AWS edge cases, outages from third-party services, unexpected user actions, and even our own code.

Our users are technical, so it’s often beneficial to surface more information about the error than for a typical consumer app. These errors can originate from vastly different parts of the system. It’s critical to have a robust pattern to handle and display these throughout the app.

In this post, I detail the error system instituted by our senior engineer, Camila Rondinini.

## Goals for the error system

-   Gracefully handle all errors in a consistent way

-   Every error has a unique code for grep-ability

-   Users should immediately understand what happened and how to resolve it by displaying help info or links for resolving the issue

-   Track which part of the system the error originated from

-   Display all expected error information to users

-   Display only minimal details about unexpected errors and silently notify our engineering team

-   Easy for our developers to handle errors

-   Full type safety in our code

-   Centralized error dictionary

-   Store all errors in the database in a normalized format

-   Ability to retroactively change the displayed user content for errors that occurred in the past

## Typescript error system

To satisfy the above requirements, we need to track and store six pieces of information:

1.  **Fault**: the high level system from which the error originated. Examples: Flightcontrol, AWS.

2.  **Grouping**: the sub-category within that system. For Flightcontrol, that is normally our domain models like Deployment and Project. For AWS, it is specific AWS services like ECS.

3.  **Code**: a unique and structured short string about the error.

4.  **Original error**: the original error.

5.  **Data**: additional data for displaying to the user.

6.  **Metadata**: extra information primarily for internal debugging.

We then store that information in the database in an `error` field. Most of our primary models/tables have an `error` field.

### Defining errors

Code samples are available to [view and copy here](https://codesandbox.io/p/devbox/5fw323?file=/exampleUsage.test.ts:17,32).

I’ll explain the main parts here.

At the top level, we have a `Fault` enum for the major system parts.

```typescript
1// codes/faults.ts
2export enum Fault {
3  AWS = "Aws",
4  FC = "FC",
5  Github = "Github",
6  ThirdParty = "ThirdParty",
7}
```

The `ErrorType` type describes the serialized data as stored in the database.

```typescript
1// lib/schema.ts
2export type ErrorType<F extends Fault> = {
3  fault: F
4  grouping: string
5  code: string
6  type: string
7  data?: Record<string, string | Record<string, string>>
8  metadata?: Record<string, string | Record<string, string>>
9}
```

The `codes/` folder has a file for each fault that describes the groupings and possible errors. This user facing error description info is not stored in the database, so updating it will apply to past errors as well.

```typescript
1// codes/aws.ts
2
3// Groupings
4enum Service {
5  ECS = "ECS",
6  FARGATE = "Fargate",
7  IAM = "Iam",
8  CLOUDFRONT = "CLOUDFRONT",
9  CODEBUILD = "CODEBUILD",
10  VPC = "VPC",
11}
12
13// Possible errors
14const Messages = {
15  [Service.IAM]: {
16    ACCESS_DENIED: ({ awsAccountId }: { awsAccountId?: string }) => ({
17      message: "AWS access denied",
18      description: `We are receiving Access Denied error from AWS. Please check if you have removed our authentication CloudFormation named "flightcontrol-access-${awsAccountId}" in "us-east-1". If this is unexpected, contact us for help.`,
19      action: {
20        label: "Contact support",
21        url: "https://www.flightcontrol.dev/docs/troubleshooting/contacting-support",
22      },
23    }),
24  },
25  //...
26}
```

### Creating errors

Custom errors are created like this:

```typescript
1import {Errors} from '@/error-library'
2
3try {
4 // ...
5} catch(error) {
6  throw new Errors.Aws.Iam.ACCESS_DENIED({
7    data: {
8      awsAccountId: awsAccount.id
9    },
10    metadata: {
11      timestamp: new Date().toISOString()
12    },
13    originalError: error,
14  })
15}
```

And then saved in the database like so:

```typescript
1await prisma.project.update({
2  where: {
3    id: projectId,
4  },
5  data: {
6    status: 'ERROR',
7    error: customError.getData(),
8  },
9})
```

`error.getData()` returns a plain JavaScript object:

```json
1{
2  "fault": "Aws",
3  "grouping": "IAM",
4  "code": "ACCESS_DENIED",
5  "type": "AwsIamError",
6  "data": {"awsAccountId": "189071383913"},
7  "metadata": {timestamp: "2024-02-02T17:47:47.604Z"}
8  "originalError": {/.../},
9}
```

### Displaying errors

For displaying errors to the user:

```typescript
1import {formatModelErrorForDisplay} from '@/error-library'
2
3const userFacingErrorData = formatModelErrorForDisplay(project.error)
```

The user facing error data has this structure:

```typescript
1export type UserFacingErrorData = {
2  message: string
3  code?: string
4  description?: string
5  action?: {url: string; label: string}
6}
7
8// Example
9const userFacingErrorData = {
10  "code": "AWS:IAM:Error:ACCESS_DENIED",
11  "message": "AWS access denied",
12  "description": "We are receiving Access Denied error from AWS. Please check if you have removed our authentication CloudFormation named "flightcontrol-access-123456789" in "us-east-1". If this is unexpected, contact us for help.",
13  "action": {
14    "label": "Contact support",
15    "url": "https://www.flightcontrol.dev/docs/troubleshooting/contacting-support",
16  },
17}
```

Here’s how we display the error in Flightcontrol:

![the error card as displayed in the Flightcontrol dashboard](https://www.datocms-assets.com/98758/1708008716-fc-error-card.png)

## Summary

Some [code is available here](https://codesandbox.io/p/devbox/5fw323?file=/exampleUsage.test.ts:17,32) for you to view or copy and paste.

Our implementation in Flightcontrol is not perfect by any means. There are still many errors that need improvement. But this system makes it trivial for us to continue improving errors over time.

Let me know on [Twitter](https://twitter.com/flybayer) or [LinkedIn](https://www.linkedin.com/in/brandonbayer1/) if you’ve found this helpful!

## The easiest way to use AWS. For agents and humans.

Get a Vercel-like experience in your own AWS account. You and your agents get one place to manage and monitor Terraform resources, builds, and deploys.

[Start free](https://app.ravion.com/signup)

[Book a demo](https://www.ravion.com/demo)

Free to start / No credit card / Revoke access anytime
