diff --git a/integration-tests/tests/cli/__snapshots__/schema.spec.ts.snap b/integration-tests/tests/cli/__snapshots__/schema.spec.ts.snap
index e04ac2cd6f..fc13c18df5 100644
--- a/integration-tests/tests/cli/__snapshots__/schema.spec.ts.snap
+++ b/integration-tests/tests/cli/__snapshots__/schema.spec.ts.snap
@@ -96,10 +96,13 @@ exports[`FEDERATION > publishing invalid schema SDL provides meaningful feedback
 exitCode------------------------------------------:
 2
 stderr--------------------------------------------:
-    Error: The SDL is not valid at line 1, column 1:
-     Syntax Error: Unexpected Name "iliketurtles".
+ ›   Error: The SDL is not valid at line 1, column 1:
+ ›    Syntax Error: Unexpected Name "iliketurtles".  [301]
+ ›   > See https://__URL__ for
+ ›    a complete list of error codes and recommended fixes.
+ ›   To disable this message set HIVE_NO_ERROR_TIP=1
 stdout--------------------------------------------:
-✖ Failed to publish schema
+__NONE__
 `;
 
 exports[`FEDERATION > schema:check should notify user when registry is empty > schemaCheck 1`] = `
@@ -160,10 +163,13 @@ exports[`FEDERATION > schema:publish should see Invalid Token error when token i
 exitCode------------------------------------------:
 2
 stderr--------------------------------------------:
- ›   Error: Invalid token provided
- ›   Reference: __ID__
+ ›   Error: A valid registry token is required to perform the action. The 
+ ›   registry token used does not exist or has been revoked.  [106]
+ ›   > See https://__URL__ for
+ ›    a complete list of error codes and recommended fixes.
+ ›   To disable this message set HIVE_NO_ERROR_TIP=1
 stdout--------------------------------------------:
-✖ Failed to publish schema
+__NONE__
 `;
 
 exports[`SINGLE > can publish a schema with breaking, warning and safe changes > schemaCheck 1`] = `
@@ -246,10 +252,13 @@ exports[`SINGLE > publishing invalid schema SDL provides meaningful feedback for
 exitCode------------------------------------------:
 2
 stderr--------------------------------------------:
-    Error: The SDL is not valid at line 1, column 1:
-     Syntax Error: Unexpected Name "iliketurtles".
+ ›   Error: The SDL is not valid at line 1, column 1:
+ ›    Syntax Error: Unexpected Name "iliketurtles".  [301]
+ ›   > See https://__URL__ for
+ ›    a complete list of error codes and recommended fixes.
+ ›   To disable this message set HIVE_NO_ERROR_TIP=1
 stdout--------------------------------------------:
-✖ Failed to publish schema
+__NONE__
 `;
 
 exports[`SINGLE > schema:check should notify user when registry is empty > schemaCheck 1`] = `
@@ -310,10 +319,13 @@ exports[`SINGLE > schema:publish should see Invalid Token error when token is in
 exitCode------------------------------------------:
 2
 stderr--------------------------------------------:
- ›   Error: Invalid token provided
- ›   Reference: __ID__
+ ›   Error: A valid registry token is required to perform the action. The 
+ ›   registry token used does not exist or has been revoked.  [106]
+ ›   > See https://__URL__ for
+ ›    a complete list of error codes and recommended fixes.
+ ›   To disable this message set HIVE_NO_ERROR_TIP=1
 stdout--------------------------------------------:
-✖ Failed to publish schema
+__NONE__
 `;
 
 exports[`STITCHING > can publish a schema with breaking, warning and safe changes > schemaCheck 1`] = `
@@ -412,10 +424,13 @@ exports[`STITCHING > publishing invalid schema SDL provides meaningful feedback
 exitCode------------------------------------------:
 2
 stderr--------------------------------------------:
-    Error: The SDL is not valid at line 1, column 1:
-     Syntax Error: Unexpected Name "iliketurtles".
+ ›   Error: The SDL is not valid at line 1, column 1:
+ ›    Syntax Error: Unexpected Name "iliketurtles".  [301]
+ ›   > See https://__URL__ for
+ ›    a complete list of error codes and recommended fixes.
+ ›   To disable this message set HIVE_NO_ERROR_TIP=1
 stdout--------------------------------------------:
-✖ Failed to publish schema
+__NONE__
 `;
 
 exports[`STITCHING > schema:check should notify user when registry is empty > schemaCheck 1`] = `
@@ -478,8 +493,11 @@ exports[`STITCHING > schema:publish should see Invalid Token error when token is
 exitCode------------------------------------------:
 2
 stderr--------------------------------------------:
- ›   Error: Invalid token provided
- ›   Reference: __ID__
+ ›   Error: A valid registry token is required to perform the action. The 
+ ›   registry token used does not exist or has been revoked.  [106]
+ ›   > See https://__URL__ for
+ ›    a complete list of error codes and recommended fixes.
+ ›   To disable this message set HIVE_NO_ERROR_TIP=1
 stdout--------------------------------------------:
-✖ Failed to publish schema
+__NONE__
 `;
diff --git a/packages/libraries/cli/src/base-command.ts b/packages/libraries/cli/src/base-command.ts
index 57db9bf6aa..cab6f5b535 100644
--- a/packages/libraries/cli/src/base-command.ts
+++ b/packages/libraries/cli/src/base-command.ts
@@ -1,9 +1,21 @@
-import { print, type GraphQLError } from 'graphql';
+import { existsSync, readFileSync } from 'node:fs';
+import { env } from 'node:process';
+import { print } from 'graphql';
 import type { ExecutionResult } from 'graphql';
 import { http } from '@graphql-hive/core';
 import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
-import { Command, Errors, Flags, Interfaces } from '@oclif/core';
+import { Command, Flags, Interfaces } from '@oclif/core';
 import { Config, GetConfigurationValueType, ValidConfigurationKeys } from './helpers/config';
+import {
+  APIError,
+  FileMissingError,
+  HTTPError,
+  InvalidFileContentsError,
+  InvalidRegistryTokenError,
+  isAggregateError,
+  MissingArgumentsError,
+  NetworkError,
+} from './helpers/errors';
 import { Texture } from './helpers/texture/texture';
 
 export type Flags<T extends typeof Command> = Interfaces.InferredFlags<
@@ -57,7 +69,7 @@ export default abstract class BaseCommand<T extends typeof Command> extends Comm
   }
 
   logFailure(...args: any[]) {
-    this.log(Texture.failure(...args));
+    this.logToStderr(Texture.failure(...args));
   }
 
   logInfo(...args: any[]) {
@@ -98,7 +110,7 @@ export default abstract class BaseCommand<T extends typeof Command> extends Comm
    * @param key
    * @param args all arguments or flags
    * @param defaultValue default value
-   * @param message custom error message in case of no value
+   * @param description description of the flag in case of no value
    * @param env an env var name
    */
   ensure<
@@ -111,8 +123,8 @@ export default abstract class BaseCommand<T extends typeof Command> extends Comm
     args,
     legacyFlagName,
     defaultValue,
-    message,
-    env,
+    env: envName,
+    description,
   }: {
     args: TArgs;
     key: TKey;
@@ -127,38 +139,34 @@ export default abstract class BaseCommand<T extends typeof Command> extends Comm
     }>;
 
     defaultValue?: TArgs[keyof TArgs] | null;
-    message?: string;
+    description: string;
     env?: string;
   }): NonNullable<GetConfigurationValueType<TKey>> | never {
-    if (args[key] != null) {
-      return args[key] as NonNullable<GetConfigurationValueType<TKey>>;
-    }
-
-    if (legacyFlagName && (args as any)[legacyFlagName] != null) {
-      return args[legacyFlagName] as any as NonNullable<GetConfigurationValueType<TKey>>;
-    }
-
-    // eslint-disable-next-line no-process-env
-    if (env && process.env[env]) {
-      // eslint-disable-next-line no-process-env
-      return process.env[env] as TArgs[keyof TArgs] as NonNullable<GetConfigurationValueType<TKey>>;
-    }
-
-    const userConfigValue = this._userConfig!.get(key);
-
-    if (userConfigValue != null) {
-      return userConfigValue;
-    }
+    let value: GetConfigurationValueType<TKey>;
 
-    if (defaultValue) {
-      return defaultValue;
+    if (args[key] != null) {
+      value = args[key];
+    } else if (legacyFlagName && (args as any)[legacyFlagName] != null) {
+      value = args[legacyFlagName] as NonNullable<GetConfigurationValueType<TKey>>;
+    } else if (envName && env[envName] !== undefined) {
+      value = env[envName] as TArgs[keyof TArgs] as NonNullable<GetConfigurationValueType<TKey>>;
+    } else {
+      const configValue = this._userConfig!.get(key) as NonNullable<
+        GetConfigurationValueType<TKey>
+      >;
+
+      if (configValue !== undefined) {
+        value = configValue;
+      } else if (defaultValue) {
+        value = defaultValue;
+      }
     }
 
-    if (message) {
-      throw new Errors.CLIError(message);
+    if (value?.length) {
+      return value;
     }
 
-    throw new Errors.CLIError(`Missing "${String(key)}"`);
+    throw new MissingArgumentsError([String(key), description]);
   }
 
   cleanRequestId(requestId?: string | null) {
@@ -186,7 +194,7 @@ export default abstract class BaseCommand<T extends typeof Command> extends Comm
     const isDebug = this.flags.debug;
 
     return {
-      async request<TResult, TVariables>(
+      request: async <TResult, TVariables>(
         args: {
           operation: TypedDocumentNode<TResult, TVariables>;
           /** timeout in milliseconds */
@@ -198,43 +206,72 @@ export default abstract class BaseCommand<T extends typeof Command> extends Comm
           : {
               variables: TVariables;
             }),
-      ): Promise<TResult> {
-        const response = await http.post(
-          endpoint,
-          JSON.stringify({
-            query: typeof args.operation === 'string' ? args.operation : print(args.operation),
-            variables: args.variables,
-          }),
-          {
-            logger: {
-              info: (...args) => {
-                if (isDebug) {
-                  console.info(...args);
-                }
-              },
-              error: (...args) => {
-                console.error(...args);
+      ): Promise<TResult> => {
+        let response: Response;
+        try {
+          response = await http.post(
+            endpoint,
+            JSON.stringify({
+              query: typeof args.operation === 'string' ? args.operation : print(args.operation),
+              variables: args.variables,
+            }),
+            {
+              logger: {
+                info: (...args) => {
+                  if (isDebug) {
+                    this.logInfo(...args);
+                  }
+                },
+                error: (...args) => {
+                  // Allow retrying requests without noise
+                  if (isDebug) {
+                    this.logWarning(...args);
+                  }
+                },
               },
+              headers: requestHeaders,
+              timeout: args.timeout,
             },
-            headers: requestHeaders,
-            timeout: args.timeout,
-          },
-        );
+          );
+        } catch (e: any) {
+          const sourceError = e?.cause ?? e;
+          if (isAggregateError(sourceError)) {
+            throw new NetworkError(sourceError.errors[0]?.message);
+          } else {
+            throw new NetworkError(sourceError);
+          }
+        }
 
         if (!response.ok) {
-          throw new Error(`Invalid status code for HTTP call: ${response.status}`);
+          throw new HTTPError(
+            endpoint,
+            response.status,
+            response.statusText ?? 'Invalid status code for HTTP call',
+          );
+        }
+
+        let jsonData;
+        try {
+          jsonData = (await response.json()) as ExecutionResult<TResult>;
+        } catch (err) {
+          const contentType = response?.headers?.get('content-type');
+          throw new APIError(
+            `Response from graphql was not valid JSON.${contentType ? ` Received "content-type": "${contentType}".` : ''}`,
+            this.cleanRequestId(response?.headers?.get('x-request-id')),
+          );
         }
-        const jsonData = (await response.json()) as ExecutionResult<TResult>;
 
         if (jsonData.errors && jsonData.errors.length > 0) {
-          throw new ClientError(
-            `Failed to execute GraphQL operation: ${jsonData.errors
-              .map(e => e.message)
-              .join('\n')}`,
-            {
-              errors: jsonData.errors,
-              headers: response.headers,
-            },
+          if (jsonData.errors[0].message === 'Invalid token provided') {
+            throw new InvalidRegistryTokenError();
+          }
+
+          if (isDebug) {
+            this.logFailure(jsonData.errors);
+          }
+          throw new APIError(
+            jsonData.errors.map(e => e.message).join('\n'),
+            this.cleanRequestId(response?.headers?.get('x-request-id')),
           );
         }
 
@@ -243,32 +280,6 @@ export default abstract class BaseCommand<T extends typeof Command> extends Comm
     };
   }
 
-  handleFetchError(error: unknown): never {
-    if (typeof error === 'string') {
-      return this.error(error);
-    }
-
-    if (error instanceof Error) {
-      if (isClientError(error)) {
-        const errors = error.response?.errors;
-
-        if (Array.isArray(errors) && errors.length > 0) {
-          return this.error(errors[0].message, {
-            ref: this.cleanRequestId(error.response?.headers?.get('x-request-id')),
-          });
-        }
-
-        return this.error(error.message, {
-          ref: this.cleanRequestId(error.response?.headers?.get('x-request-id')),
-        });
-      }
-
-      return this.error(error);
-    }
-
-    return this.error(JSON.stringify(error));
-  }
-
   async require<
     TFlags extends {
       require: string[];
@@ -281,20 +292,25 @@ export default abstract class BaseCommand<T extends typeof Command> extends Comm
       );
     }
   }
-}
 
-class ClientError extends Error {
-  constructor(
-    message: string,
-    public response: {
-      errors?: readonly GraphQLError[];
-      headers: Headers;
-    },
-  ) {
-    super(message);
-  }
-}
+  readJSON(file: string): string {
+    // If we can't parse it, we can try to load it from FS
+    const exists = existsSync(file);
 
-function isClientError(error: Error): error is ClientError {
-  return error instanceof ClientError;
+    if (!exists) {
+      throw new FileMissingError(
+        file,
+        'Please specify a path to an existing file, or a string with valid JSON',
+      );
+    }
+
+    try {
+      const fileContent = readFileSync(file, 'utf-8');
+      JSON.parse(fileContent);
+
+      return fileContent;
+    } catch (e) {
+      throw new InvalidFileContentsError(file, 'JSON');
+    }
+  }
 }
diff --git a/packages/libraries/cli/src/commands/app/create.ts b/packages/libraries/cli/src/commands/app/create.ts
index 36956b8273..ef7e8a10bb 100644
--- a/packages/libraries/cli/src/commands/app/create.ts
+++ b/packages/libraries/cli/src/commands/app/create.ts
@@ -4,7 +4,12 @@ import Command from '../../base-command';
 import { graphql } from '../../gql';
 import { AppDeploymentStatus } from '../../gql/graphql';
 import { graphqlEndpoint } from '../../helpers/config';
-import { ACCESS_TOKEN_MISSING } from '../../helpers/errors';
+import {
+  APIError,
+  MissingEndpointError,
+  MissingRegistryTokenError,
+  PersistedOperationsMalformedError,
+} from '../../helpers/errors';
 
 export default class AppCreate extends Command<typeof AppCreate> {
   static description = 'create an app deployment';
@@ -37,28 +42,37 @@ export default class AppCreate extends Command<typeof AppCreate> {
   async run() {
     const { flags, args } = await this.parse(AppCreate);
 
-    const endpoint = this.ensure({
-      key: 'registry.endpoint',
-      args: flags,
-      defaultValue: graphqlEndpoint,
-      env: 'HIVE_REGISTRY',
-    });
-    const accessToken = this.ensure({
-      key: 'registry.accessToken',
-      args: flags,
-      env: 'HIVE_TOKEN',
-      message: ACCESS_TOKEN_MISSING,
-    });
+    let endpoint: string, accessToken: string;
+    try {
+      endpoint = this.ensure({
+        key: 'registry.endpoint',
+        args: flags,
+        defaultValue: graphqlEndpoint,
+        env: 'HIVE_REGISTRY',
+        description: AppCreate.flags['registry.endpoint'].description!,
+      });
+    } catch (e) {
+      throw new MissingEndpointError();
+    }
+
+    try {
+      accessToken = this.ensure({
+        key: 'registry.accessToken',
+        args: flags,
+        env: 'HIVE_TOKEN',
+        description: AppCreate.flags['registry.accessToken'].description!,
+      });
+    } catch (e) {
+      throw new MissingRegistryTokenError();
+    }
 
     const file: string = args.file;
-    const fs = await import('fs/promises');
-    const contents = await fs.readFile(file, 'utf-8');
+    const contents = this.readJSON(file);
     const operations: unknown = JSON.parse(contents);
     const validationResult = ManifestModel.safeParse(operations);
 
     if (validationResult.success === false) {
-      // TODO: better error message :)
-      throw new Error('Invalid manifest');
+      throw new PersistedOperationsMalformedError(file);
     }
 
     const result = await this.registryApi(endpoint, accessToken).request({
@@ -72,12 +86,11 @@ export default class AppCreate extends Command<typeof AppCreate> {
     });
 
     if (result.createAppDeployment.error) {
-      // TODO: better error message formatting :)
-      throw new Error(result.createAppDeployment.error.message);
+      throw new APIError(result.createAppDeployment.error.message);
     }
 
     if (!result.createAppDeployment.ok) {
-      throw new Error('Unknown error');
+      throw new APIError(`Create App failed without providing a reason.`);
     }
 
     if (result.createAppDeployment.ok.createdAppDeployment.status !== AppDeploymentStatus.Pending) {
@@ -123,7 +136,7 @@ export default class AppCreate extends Command<typeof AppCreate> {
               );
             }
           }
-          this.error(result.addDocumentsToAppDeployment.error.message);
+          throw new APIError(result.addDocumentsToAppDeployment.error.message);
         }
         buffer = [];
       }
diff --git a/packages/libraries/cli/src/commands/app/publish.ts b/packages/libraries/cli/src/commands/app/publish.ts
index 45a990be9a..663c7a92e0 100644
--- a/packages/libraries/cli/src/commands/app/publish.ts
+++ b/packages/libraries/cli/src/commands/app/publish.ts
@@ -2,7 +2,7 @@ import { Flags } from '@oclif/core';
 import Command from '../../base-command';
 import { graphql } from '../../gql';
 import { graphqlEndpoint } from '../../helpers/config';
-import { ACCESS_TOKEN_MISSING } from '../../helpers/errors';
+import { APIError, MissingEndpointError, MissingRegistryTokenError } from '../../helpers/errors';
 
 export default class AppPublish extends Command<typeof AppPublish> {
   static description = 'publish an app deployment';
@@ -26,18 +26,29 @@ export default class AppPublish extends Command<typeof AppPublish> {
   async run() {
     const { flags } = await this.parse(AppPublish);
 
-    const endpoint = this.ensure({
-      key: 'registry.endpoint',
-      args: flags,
-      defaultValue: graphqlEndpoint,
-      env: 'HIVE_REGISTRY',
-    });
-    const accessToken = this.ensure({
-      key: 'registry.accessToken',
-      args: flags,
-      env: 'HIVE_TOKEN',
-      message: ACCESS_TOKEN_MISSING,
-    });
+    let endpoint: string, accessToken: string;
+    try {
+      endpoint = this.ensure({
+        key: 'registry.endpoint',
+        args: flags,
+        defaultValue: graphqlEndpoint,
+        env: 'HIVE_REGISTRY',
+        description: AppPublish.flags['registry.endpoint'].description!,
+      });
+    } catch (e) {
+      throw new MissingEndpointError();
+    }
+
+    try {
+      accessToken = this.ensure({
+        key: 'registry.accessToken',
+        args: flags,
+        env: 'HIVE_TOKEN',
+        description: AppPublish.flags['registry.accessToken'].description!,
+      });
+    } catch (e) {
+      throw new MissingRegistryTokenError();
+    }
 
     const result = await this.registryApi(endpoint, accessToken).request({
       operation: ActivateAppDeploymentMutation,
@@ -50,7 +61,7 @@ export default class AppPublish extends Command<typeof AppPublish> {
     });
 
     if (result.activateAppDeployment.error) {
-      throw new Error(result.activateAppDeployment.error.message);
+      throw new APIError(result.activateAppDeployment.error.message);
     }
 
     if (result.activateAppDeployment.ok) {
diff --git a/packages/libraries/cli/src/commands/artifact/fetch.ts b/packages/libraries/cli/src/commands/artifact/fetch.ts
index 3677b35710..5fac9d31f9 100644
--- a/packages/libraries/cli/src/commands/artifact/fetch.ts
+++ b/packages/libraries/cli/src/commands/artifact/fetch.ts
@@ -1,6 +1,14 @@
 import { http, URL } from '@graphql-hive/core';
 import { Flags } from '@oclif/core';
 import Command from '../../base-command';
+import {
+  HTTPError,
+  isAggregateError,
+  MissingCdnEndpointError,
+  MissingCdnKeyError,
+  NetworkError,
+  UnexpectedError,
+} from '../../helpers/errors';
 
 export default class ArtifactsFetch extends Command<typeof ArtifactsFetch> {
   static description = 'fetch artifacts from the CDN';
@@ -24,55 +32,86 @@ export default class ArtifactsFetch extends Command<typeof ArtifactsFetch> {
   async run() {
     const { flags } = await this.parse(ArtifactsFetch);
 
-    const cdnEndpoint = this.ensure({
-      key: 'cdn.endpoint',
-      args: flags,
-      env: 'HIVE_CDN_ENDPOINT',
-    });
+    let cdnEndpoint: string, token: string;
+    try {
+      cdnEndpoint = this.ensure({
+        key: 'cdn.endpoint',
+        args: flags,
+        env: 'HIVE_CDN_ENDPOINT',
+        description: ArtifactsFetch.flags['cdn.endpoint'].description!,
+      });
+    } catch (e) {
+      throw new MissingCdnEndpointError();
+    }
 
-    const token = this.ensure({
-      key: 'cdn.accessToken',
-      args: flags,
-      env: 'HIVE_CDN_ACCESS_TOKEN',
-    });
+    try {
+      token = this.ensure({
+        key: 'cdn.accessToken',
+        args: flags,
+        env: 'HIVE_CDN_ACCESS_TOKEN',
+        description: ArtifactsFetch.flags['cdn.accessToken'].description!,
+      });
+    } catch (e) {
+      throw new MissingCdnKeyError();
+    }
 
     const artifactType = flags.artifact;
 
     const url = new URL(`${cdnEndpoint}/${artifactType}`);
 
-    const response = await http.get(url.toString(), {
-      headers: {
-        'x-hive-cdn-key': token,
-        'User-Agent': `hive-cli/${this.config.version}`,
-      },
-      retry: {
-        retries: 3,
-      },
-      logger: {
-        info: (...args) => {
-          if (this.flags.debug) {
-            console.info(...args);
-          }
+    let response;
+    try {
+      response = await http.get(url.toString(), {
+        headers: {
+          'x-hive-cdn-key': token,
+          'User-Agent': `hive-cli/${this.config.version}`,
+        },
+        retry: {
+          retries: 3,
         },
-        error: (...args) => {
-          console.error(...args);
+        logger: {
+          info: (...args) => {
+            if (this.flags.debug) {
+              console.info(...args);
+            }
+          },
+          error: (...args) => {
+            if (this.flags.debug) {
+              console.error(...args);
+            }
+          },
         },
-      },
-    });
+      });
+    } catch (e: any) {
+      const sourceError = e?.cause ?? e;
+      if (isAggregateError(sourceError)) {
+        throw new NetworkError(sourceError.errors[0]?.message);
+      } else {
+        throw new NetworkError(sourceError);
+      }
+    }
 
-    if (response.status >= 300) {
+    if (!response.ok) {
       const responseBody = await response.text();
-      throw new Error(responseBody);
+      throw new HTTPError(
+        url.toString(),
+        response.status,
+        responseBody ?? response.statusText ?? 'Invalid status code for HTTP call',
+      );
     }
 
-    if (flags.outputFile) {
-      const fs = await import('fs/promises');
-      const contents = Buffer.from(await response.arrayBuffer());
-      await fs.writeFile(flags.outputFile, contents);
-      this.log(`Wrote ${contents.length} bytes to ${flags.outputFile}`);
-      return;
-    }
+    try {
+      if (flags.outputFile) {
+        const fs = await import('fs/promises');
+        const contents = Buffer.from(await response.arrayBuffer());
+        await fs.writeFile(flags.outputFile, contents);
+        this.log(`Wrote ${contents.length} bytes to ${flags.outputFile}`);
+        return;
+      }
 
-    this.log(await response.text());
+      this.log(await response.text());
+    } catch (e) {
+      throw new UnexpectedError(e);
+    }
   }
 }
diff --git a/packages/libraries/cli/src/commands/dev.ts b/packages/libraries/cli/src/commands/dev.ts
index 17aee31150..77c93977dd 100644
--- a/packages/libraries/cli/src/commands/dev.ts
+++ b/packages/libraries/cli/src/commands/dev.ts
@@ -11,8 +11,19 @@ import {
 import Command from '../base-command';
 import { graphql } from '../gql';
 import { graphqlEndpoint } from '../helpers/config';
-import { ACCESS_TOKEN_MISSING } from '../helpers/errors';
-import { loadSchema, renderErrors } from '../helpers/schema';
+import {
+  APIError,
+  HiveCLIError,
+  IntrospectionError,
+  InvalidCompositionResultError,
+  LocalCompositionError,
+  MissingEndpointError,
+  MissingRegistryTokenError,
+  RemoteCompositionError,
+  ServiceAndUrlLengthMismatch,
+  UnexpectedError,
+} from '../helpers/errors';
+import { loadSchema } from '../helpers/schema';
 import { invariant } from '../helpers/validation';
 
 const CLI_SchemaComposeMutation = graphql(/* GraphQL */ `
@@ -173,9 +184,7 @@ export default class Dev extends Command<typeof Dev> {
     const { unstable__forceLatest } = flags;
 
     if (flags.service.length !== flags.url.length) {
-      this.error('Not every services has a matching url', {
-        exit: 1,
-      });
+      throw new ServiceAndUrlLengthMismatch(flags.service, flags.url);
     }
 
     const isRemote = flags.remote === true;
@@ -193,20 +202,30 @@ export default class Dev extends Command<typeof Dev> {
 
     if (flags.watch === true) {
       if (isRemote) {
-        const registry = this.ensure({
-          key: 'registry.endpoint',
-          legacyFlagName: 'registry',
-          args: flags,
-          defaultValue: graphqlEndpoint,
-          env: 'HIVE_REGISTRY',
-        });
-        const token = this.ensure({
-          key: 'registry.accessToken',
-          legacyFlagName: 'token',
-          args: flags,
-          env: 'HIVE_TOKEN',
-          message: ACCESS_TOKEN_MISSING,
-        });
+        let registry: string, token: string;
+        try {
+          registry = this.ensure({
+            key: 'registry.endpoint',
+            legacyFlagName: 'registry',
+            args: flags,
+            defaultValue: graphqlEndpoint,
+            env: 'HIVE_REGISTRY',
+            description: Dev.flags['registry.endpoint'].description!,
+          });
+        } catch (e) {
+          throw new MissingEndpointError();
+        }
+        try {
+          token = this.ensure({
+            key: 'registry.accessToken',
+            legacyFlagName: 'token',
+            args: flags,
+            env: 'HIVE_TOKEN',
+            description: Dev.flags['registry.accessToken'].description!,
+          });
+        } catch (e) {
+          throw new MissingRegistryTokenError();
+        }
 
         void this.watch(flags.watchInterval, serviceInputs, services =>
           this.compose({
@@ -215,8 +234,9 @@ export default class Dev extends Command<typeof Dev> {
             token,
             write: flags.write,
             unstable__forceLatest,
-            onError: message => {
-              this.logFailure(message);
+            onError: error => {
+              // watch mode should not exit. Log instead.
+              this.logFailure(error.message);
             },
           }),
         );
@@ -228,8 +248,9 @@ export default class Dev extends Command<typeof Dev> {
         this.composeLocally({
           services,
           write: flags.write,
-          onError: message => {
-            this.logFailure(message);
+          onError: error => {
+            // watch mode should not exit. Log instead.
+            this.logFailure(error.message);
           },
         }),
       );
@@ -239,20 +260,30 @@ export default class Dev extends Command<typeof Dev> {
     const services = await this.resolveServices(serviceInputs);
 
     if (isRemote) {
-      const registry = this.ensure({
-        key: 'registry.endpoint',
-        legacyFlagName: 'registry',
-        args: flags,
-        defaultValue: graphqlEndpoint,
-        env: 'HIVE_REGISTRY',
-      });
-      const token = this.ensure({
-        key: 'registry.accessToken',
-        legacyFlagName: 'token',
-        args: flags,
-        env: 'HIVE_TOKEN',
-        message: ACCESS_TOKEN_MISSING,
-      });
+      let registry: string, token: string;
+      try {
+        registry = this.ensure({
+          key: 'registry.endpoint',
+          legacyFlagName: 'registry',
+          args: flags,
+          defaultValue: graphqlEndpoint,
+          env: 'HIVE_REGISTRY',
+          description: Dev.flags['registry.endpoint'].description!,
+        });
+      } catch (e) {
+        throw new MissingEndpointError();
+      }
+      try {
+        token = this.ensure({
+          key: 'registry.accessToken',
+          legacyFlagName: 'token',
+          args: flags,
+          env: 'HIVE_TOKEN',
+          description: Dev.flags['registry.accessToken'].description!,
+        });
+      } catch (e) {
+        throw new MissingRegistryTokenError();
+      }
 
       return this.compose({
         services,
@@ -260,10 +291,8 @@ export default class Dev extends Command<typeof Dev> {
         token,
         write: flags.write,
         unstable__forceLatest,
-        onError: message => {
-          this.error(message, {
-            exit: 1,
-          });
+        onError: error => {
+          throw error;
         },
       });
     }
@@ -271,10 +300,8 @@ export default class Dev extends Command<typeof Dev> {
     return this.composeLocally({
       services,
       write: flags.write,
-      onError: message => {
-        this.error(message, {
-          exit: 1,
-        });
+      onError: error => {
+        throw error;
       },
     });
   }
@@ -286,7 +313,7 @@ export default class Dev extends Command<typeof Dev> {
       sdl: string;
     }>;
     write: string;
-    onError: (message: string) => void | never;
+    onError: (error: HiveCLIError) => void | never;
   }) {
     const compositionResult = await new Promise<CompositionResult>((resolve, reject) => {
       try {
@@ -300,32 +327,15 @@ export default class Dev extends Command<typeof Dev> {
           ),
         );
       } catch (error) {
+        // @note: composeServices should not throw.
+        // This reject is for the offchance that something happens under the hood that was not expected.
+        // Without it, if something happened then the promise would hang.
         reject(error);
       }
-    }).catch(error => {
-      this.handleFetchError(error);
     });
 
     if (compositionHasErrors(compositionResult)) {
-      if (compositionResult.errors) {
-        this.log(
-          renderErrors({
-            total: compositionResult.errors.length,
-            nodes: compositionResult.errors.map(error => ({
-              message: error.message,
-            })),
-          }),
-        );
-      }
-
-      input.onError('Composition failed');
-      return;
-    }
-
-    if (typeof compositionResult.supergraphSdl !== 'string') {
-      input.onError(
-        'Composition successful but failed to get supergraph schema. Please try again later or contact support',
-      );
+      input.onError(new LocalCompositionError(compositionResult));
       return;
     }
 
@@ -344,52 +354,56 @@ export default class Dev extends Command<typeof Dev> {
     token: string;
     write: string;
     unstable__forceLatest: boolean;
-    onError: (message: string) => void | never;
+    onError: (error: HiveCLIError) => void | never;
   }) {
-    const result = await this.registryApi(input.registry, input.token)
-      .request({
-        operation: CLI_SchemaComposeMutation,
-        variables: {
-          input: {
-            useLatestComposableVersion: !input.unstable__forceLatest,
-            services: input.services.map(service => ({
-              name: service.name,
-              url: service.url,
-              sdl: service.sdl,
-            })),
-          },
+    const result = await this.registryApi(input.registry, input.token).request({
+      operation: CLI_SchemaComposeMutation,
+      variables: {
+        input: {
+          useLatestComposableVersion: !input.unstable__forceLatest,
+          services: input.services.map(service => ({
+            name: service.name,
+            url: service.url,
+            sdl: service.sdl,
+          })),
         },
-      })
-      .catch(error => {
-        this.handleFetchError(error);
-      });
+      },
+    });
 
     if (result.schemaCompose.__typename === 'SchemaComposeError') {
-      input.onError(result.schemaCompose.message);
+      input.onError(new APIError(result.schemaCompose.message));
       return;
     }
 
     const { valid, compositionResult } = result.schemaCompose;
 
     if (!valid) {
+      // @note: Can this actually be invalid without any errors?
       if (compositionResult.errors) {
-        this.log(renderErrors(compositionResult.errors));
+        input.onError(new RemoteCompositionError(compositionResult.errors));
+        return;
       }
 
-      input.onError('Composition failed');
+      input.onError(new InvalidCompositionResultError(compositionResult.supergraphSdl));
       return;
     }
 
     if (typeof compositionResult.supergraphSdl !== 'string') {
-      input.onError(
-        'Composition successful but failed to get supergraph schema. Please try again later or contact support',
-      );
+      input.onError(new InvalidCompositionResultError(compositionResult.supergraphSdl));
       return;
     }
 
     this.logSuccess('Composition successful');
     this.log(`Saving supergraph schema to ${input.write}`);
-    await writeFile(resolve(process.cwd(), input.write), compositionResult.supergraphSdl, 'utf-8');
+    try {
+      await writeFile(
+        resolve(process.cwd(), input.write),
+        compositionResult.supergraphSdl,
+        'utf-8',
+      );
+    } catch (e) {
+      input.onError(new UnexpectedError(e));
+    }
   }
 
   private async watch(
@@ -399,8 +413,13 @@ export default class Dev extends Command<typeof Dev> {
   ) {
     this.logInfo('Watch mode enabled');
 
-    let services = await this.resolveServices(serviceInputs);
-    await compose(services);
+    let services: ServiceWithSource[];
+    try {
+      services = await this.resolveServices(serviceInputs);
+      await compose(services);
+    } catch (e) {
+      throw new UnexpectedError(e);
+    }
 
     this.logInfo('Watching for changes');
 
@@ -424,7 +443,7 @@ export default class Dev extends Command<typeof Dev> {
           services = newServices;
         }
       } catch (error) {
-        this.logFailure(String(error));
+        this.logFailure(new UnexpectedError(error));
       }
 
       timeoutId = setTimeout(watch, watchInterval);
@@ -483,16 +502,12 @@ export default class Dev extends Command<typeof Dev> {
   }
 
   private async resolveSdlFromUrl(url: string) {
-    const result = await this.graphql(url)
-      .request({ operation: ServiceIntrospectionQuery })
-      .catch(error => {
-        this.handleFetchError(error);
-      });
+    const result = await this.graphql(url).request({ operation: ServiceIntrospectionQuery });
 
     const sdl = result._service.sdl;
 
     if (!sdl) {
-      throw new Error('Failed to introspect service');
+      throw new IntrospectionError();
     }
 
     return sdl;
diff --git a/packages/libraries/cli/src/commands/introspect.ts b/packages/libraries/cli/src/commands/introspect.ts
index 3682562c91..acb586a46d 100644
--- a/packages/libraries/cli/src/commands/introspect.ts
+++ b/packages/libraries/cli/src/commands/introspect.ts
@@ -1,8 +1,9 @@
 import { writeFileSync } from 'node:fs';
 import { extname, resolve } from 'node:path';
-import { buildSchema, GraphQLError, introspectionFromSchema } from 'graphql';
+import { buildSchema, introspectionFromSchema } from 'graphql';
 import { Args, Flags } from '@oclif/core';
 import Command from '../base-command';
+import { APIError, UnexpectedError, UnsupportedFileExtensionError } from '../helpers/errors';
 import { loadSchema } from '../helpers/schema';
 
 export default class Introspect extends Command<typeof Introspect> {
@@ -46,19 +47,11 @@ export default class Introspect extends Command<typeof Introspect> {
       headers,
       method: 'POST',
     }).catch(err => {
-      if (err instanceof GraphQLError) {
-        this.logFailure(err.message);
-        this.exit(1);
-      }
-
-      this.error(err, {
-        exit: 1,
-      });
+      throw new APIError(err);
     });
 
     if (!schema) {
-      this.logFailure('Unable to load schema');
-      this.exit(1);
+      throw new UnexpectedError('Unable to load schema');
     }
 
     if (!flags.write) {
@@ -89,8 +82,7 @@ export default class Introspect extends Command<typeof Introspect> {
           break;
         }
         default:
-          this.logFailure(`Unsupported file extension ${extname(flags.write)}`);
-          this.exit(1);
+          throw new UnsupportedFileExtensionError(flags.write);
       }
 
       this.logSuccess(`Saved to ${filepath}`);
diff --git a/packages/libraries/cli/src/commands/operations/check.ts b/packages/libraries/cli/src/commands/operations/check.ts
index 7b3266d274..150c8a323e 100644
--- a/packages/libraries/cli/src/commands/operations/check.ts
+++ b/packages/libraries/cli/src/commands/operations/check.ts
@@ -1,10 +1,16 @@
-import { buildSchema, GraphQLError, Source } from 'graphql';
-import { InvalidDocument, validate } from '@graphql-inspector/core';
+import { buildSchema, Source } from 'graphql';
+import { validate } from '@graphql-inspector/core';
 import { Args, Errors, Flags } from '@oclif/core';
 import Command from '../../base-command';
 import { graphql } from '../../gql';
 import { graphqlEndpoint } from '../../helpers/config';
-import { ACCESS_TOKEN_MISSING } from '../../helpers/errors';
+import {
+  InvalidDocumentsError,
+  MissingEndpointError,
+  MissingRegistryTokenError,
+  SchemaNotFoundError,
+  UnexpectedError,
+} from '../../helpers/errors';
 import { loadOperations } from '../../helpers/operations';
 import { Texture } from '../../helpers/texture/texture';
 
@@ -84,21 +90,33 @@ export default class OperationsCheck extends Command<typeof OperationsCheck> {
       const { flags, args } = await this.parse(OperationsCheck);
 
       await this.require(flags);
+      let accessToken: string, endpoint: string;
+
+      try {
+        endpoint = this.ensure({
+          key: 'registry.endpoint',
+          args: flags,
+          legacyFlagName: 'registry',
+          defaultValue: graphqlEndpoint,
+          env: 'HIVE_REGISTRY',
+          description: OperationsCheck.flags['registry.endpoint'].description!,
+        });
+      } catch (e) {
+        throw new MissingEndpointError();
+      }
+
+      try {
+        accessToken = this.ensure({
+          key: 'registry.accessToken',
+          args: flags,
+          legacyFlagName: 'token',
+          env: 'HIVE_TOKEN',
+          description: OperationsCheck.flags['registry.accessToken'].description!,
+        });
+      } catch (e) {
+        throw new MissingRegistryTokenError();
+      }
 
-      const endpoint = this.ensure({
-        key: 'registry.endpoint',
-        args: flags,
-        legacyFlagName: 'registry',
-        defaultValue: graphqlEndpoint,
-        env: 'HIVE_REGISTRY',
-      });
-      const accessToken = this.ensure({
-        key: 'registry.accessToken',
-        args: flags,
-        legacyFlagName: 'token',
-        env: 'HIVE_TOKEN',
-        message: ACCESS_TOKEN_MISSING,
-      });
       const graphqlTag = flags.graphqlTag;
       const globalGraphqlTag = flags.globalGraphqlTag;
 
@@ -129,7 +147,7 @@ export default class OperationsCheck extends Command<typeof OperationsCheck> {
       const sdl = result.latestValidVersion?.sdl;
 
       if (!sdl) {
-        this.error('Could not find a published schema. Please publish a valid schema first.');
+        throw new SchemaNotFoundError();
       }
 
       const schema = buildSchema(sdl, {
@@ -178,29 +196,14 @@ export default class OperationsCheck extends Command<typeof OperationsCheck> {
 
       this.log(Texture.header('Details'));
 
-      this.printInvalidDocuments(operationsWithErrors);
-      this.exit(1);
+      throw new InvalidDocumentsError(operationsWithErrors);
     } catch (error) {
-      if (error instanceof Errors.ExitError) {
+      if (error instanceof Errors.CLIError) {
         throw error;
       } else {
         this.logFailure('Failed to validate operations');
-        this.handleFetchError(error);
+        throw new UnexpectedError(error);
       }
     }
   }
-
-  private printInvalidDocuments(invalidDocuments: InvalidDocument[]): void {
-    invalidDocuments.forEach(doc => {
-      this.renderErrors(doc.source.name, doc.errors);
-    });
-  }
-
-  private renderErrors(sourceName: string, errors: GraphQLError[]) {
-    this.logFailure(sourceName);
-    errors.forEach(e => {
-      this.log(` - ${Texture.boldQuotedWords(e.message)}`);
-    });
-    this.log('');
-  }
 }
diff --git a/packages/libraries/cli/src/commands/schema/check.ts b/packages/libraries/cli/src/commands/schema/check.ts
index 8a77901912..be9ac3e0af 100644
--- a/packages/libraries/cli/src/commands/schema/check.ts
+++ b/packages/libraries/cli/src/commands/schema/check.ts
@@ -3,7 +3,16 @@ import { Args, Errors, Flags } from '@oclif/core';
 import Command from '../../base-command';
 import { graphql } from '../../gql';
 import { graphqlEndpoint } from '../../helpers/config';
-import { ACCESS_TOKEN_MISSING } from '../../helpers/errors';
+import {
+  APIError,
+  GithubCommitRequiredError,
+  GithubRepositoryRequiredError,
+  MissingEndpointError,
+  MissingRegistryTokenError,
+  SchemaFileEmptyError,
+  SchemaFileNotFoundError,
+  UnexpectedError,
+} from '../../helpers/errors';
 import { gitInfo } from '../../helpers/git';
 import {
   loadSchema,
@@ -163,22 +172,35 @@ export default class SchemaCheck extends Command<typeof SchemaCheck> {
       const service = flags.service;
       const forceSafe = flags.forceSafe;
       const usesGitHubApp = flags.github === true;
-      const endpoint = this.ensure({
-        key: 'registry.endpoint',
-        args: flags,
-        legacyFlagName: 'registry',
-        defaultValue: graphqlEndpoint,
-        env: 'HIVE_REGISTRY',
-      });
+      let endpoint: string, accessToken: string;
+      try {
+        endpoint = this.ensure({
+          key: 'registry.endpoint',
+          args: flags,
+          legacyFlagName: 'registry',
+          defaultValue: graphqlEndpoint,
+          env: 'HIVE_REGISTRY',
+          description: SchemaCheck.flags['registry.endpoint'].description!,
+        });
+      } catch (e) {
+        throw new MissingEndpointError();
+      }
       const file = args.file;
-      const accessToken = this.ensure({
-        key: 'registry.accessToken',
-        args: flags,
-        legacyFlagName: 'token',
-        env: 'HIVE_TOKEN',
-        message: ACCESS_TOKEN_MISSING,
+      try {
+        accessToken = this.ensure({
+          key: 'registry.accessToken',
+          args: flags,
+          legacyFlagName: 'token',
+          env: 'HIVE_TOKEN',
+          description: SchemaCheck.flags['registry.accessToken'].description!,
+        });
+      } catch (e) {
+        throw new MissingRegistryTokenError();
+      }
+
+      const sdl = await loadSchema(file).catch(e => {
+        throw new SchemaFileNotFoundError(file, e);
       });
-      const sdl = await loadSchema(file);
       const git = await gitInfo(() => {
         // noop
       });
@@ -187,7 +209,7 @@ export default class SchemaCheck extends Command<typeof SchemaCheck> {
       const author = flags.author || git?.author;
 
       if (typeof sdl !== 'string' || sdl.length === 0) {
-        throw new Errors.CLIError('Schema seems empty');
+        throw new SchemaFileEmptyError(file);
       }
 
       let github: null | {
@@ -198,12 +220,10 @@ export default class SchemaCheck extends Command<typeof SchemaCheck> {
 
       if (usesGitHubApp) {
         if (!commit) {
-          throw new Errors.CLIError(`Couldn't resolve commit sha required for GitHub Application`);
+          throw new GithubCommitRequiredError();
         }
         if (!git.repository) {
-          throw new Errors.CLIError(
-            `Couldn't resolve git repository required for GitHub Application`,
-          );
+          throw new GithubRepositoryRequiredError();
         }
         if (!git.pullRequestNumber) {
           this.warn(
@@ -289,14 +309,14 @@ export default class SchemaCheck extends Command<typeof SchemaCheck> {
       } else if (result.schemaCheck.__typename === 'GitHubSchemaCheckSuccess') {
         this.logSuccess(result.schemaCheck.message);
       } else {
-        this.error(result.schemaCheck.message);
+        throw new APIError(result.schemaCheck.message);
       }
     } catch (error) {
-      if (error instanceof Errors.ExitError) {
+      if (error instanceof Errors.CLIError) {
         throw error;
       } else {
         this.logFailure('Failed to check schema');
-        this.handleFetchError(error);
+        throw new UnexpectedError(error);
       }
     }
   }
diff --git a/packages/libraries/cli/src/commands/schema/delete.ts b/packages/libraries/cli/src/commands/schema/delete.ts
index 7a3e729199..29bc4e3c16 100644
--- a/packages/libraries/cli/src/commands/schema/delete.ts
+++ b/packages/libraries/cli/src/commands/schema/delete.ts
@@ -2,7 +2,12 @@ import { Args, Errors, Flags, ux } from '@oclif/core';
 import Command from '../../base-command';
 import { graphql } from '../../gql';
 import { graphqlEndpoint } from '../../helpers/config';
-import { ACCESS_TOKEN_MISSING } from '../../helpers/errors';
+import {
+  APIError,
+  MissingEndpointError,
+  MissingRegistryTokenError,
+  UnexpectedError,
+} from '../../helpers/errors';
 import { renderErrors } from '../../helpers/schema';
 
 const schemaDeleteMutation = graphql(/* GraphQL */ `
@@ -99,20 +104,30 @@ export default class SchemaDelete extends Command<typeof SchemaDelete> {
         }
       }
 
-      const endpoint = this.ensure({
-        key: 'registry.endpoint',
-        args: flags,
-        legacyFlagName: 'registry',
-        defaultValue: graphqlEndpoint,
-        env: 'HIVE_REGISTRY',
-      });
-      const accessToken = this.ensure({
-        key: 'registry.accessToken',
-        args: flags,
-        legacyFlagName: 'token',
-        env: 'HIVE_TOKEN',
-        message: ACCESS_TOKEN_MISSING,
-      });
+      let accessToken: string, endpoint: string;
+      try {
+        endpoint = this.ensure({
+          key: 'registry.endpoint',
+          args: flags,
+          legacyFlagName: 'registry',
+          defaultValue: graphqlEndpoint,
+          env: 'HIVE_REGISTRY',
+          description: SchemaDelete.flags['registry.endpoint'].description!,
+        });
+      } catch (e) {
+        throw new MissingEndpointError();
+      }
+      try {
+        accessToken = this.ensure({
+          key: 'registry.accessToken',
+          args: flags,
+          legacyFlagName: 'token',
+          env: 'HIVE_TOKEN',
+          description: SchemaDelete.flags['registry.accessToken'].description!,
+        });
+      } catch (e) {
+        throw new MissingRegistryTokenError();
+      }
 
       const result = await this.registryApi(endpoint, accessToken).request({
         operation: schemaDeleteMutation,
@@ -134,15 +149,14 @@ export default class SchemaDelete extends Command<typeof SchemaDelete> {
       const errors = result.schemaDelete.errors;
 
       if (errors) {
-        this.log(renderErrors(errors));
-        this.exit(1);
+        throw new APIError(renderErrors(errors));
       }
     } catch (error) {
-      if (error instanceof Errors.ExitError) {
+      if (error instanceof Errors.CLIError) {
         throw error;
       } else {
         this.logFailure(`Failed to complete`);
-        this.handleFetchError(error);
+        throw new UnexpectedError(error);
       }
     }
   }
diff --git a/packages/libraries/cli/src/commands/schema/fetch.ts b/packages/libraries/cli/src/commands/schema/fetch.ts
index 2d2737e73e..ed4f076478 100644
--- a/packages/libraries/cli/src/commands/schema/fetch.ts
+++ b/packages/libraries/cli/src/commands/schema/fetch.ts
@@ -4,7 +4,13 @@ import { Args, Flags } from '@oclif/core';
 import Command from '../../base-command';
 import { graphql } from '../../gql';
 import { graphqlEndpoint } from '../../helpers/config';
-import { ACCESS_TOKEN_MISSING } from '../../helpers/errors';
+import {
+  InvalidSchemaError,
+  MissingEndpointError,
+  MissingRegistryTokenError,
+  SchemaNotFoundError,
+  UnsupportedFileExtensionError,
+} from '../../helpers/errors';
 import { Texture } from '../../helpers/texture/texture';
 
 const SchemaVersionForActionIdQuery = graphql(/* GraphQL */ `
@@ -119,21 +125,30 @@ export default class SchemaFetch extends Command<typeof SchemaFetch> {
   async run() {
     const { flags, args } = await this.parse(SchemaFetch);
 
-    const endpoint = this.ensure({
-      key: 'registry.endpoint',
-      args: flags,
-      env: 'HIVE_REGISTRY',
-      legacyFlagName: 'registry',
-      defaultValue: graphqlEndpoint,
-    });
-
-    const accessToken = this.ensure({
-      key: 'registry.accessToken',
-      args: flags,
-      legacyFlagName: 'token',
-      env: 'HIVE_TOKEN',
-      message: ACCESS_TOKEN_MISSING,
-    });
+    let endpoint: string, accessToken: string;
+    try {
+      endpoint = this.ensure({
+        key: 'registry.endpoint',
+        args: flags,
+        env: 'HIVE_REGISTRY',
+        legacyFlagName: 'registry',
+        defaultValue: graphqlEndpoint,
+        description: SchemaFetch.flags['registry.endpoint'].description!,
+      });
+    } catch (e) {
+      throw new MissingEndpointError();
+    }
+    try {
+      accessToken = this.ensure({
+        key: 'registry.accessToken',
+        args: flags,
+        legacyFlagName: 'token',
+        env: 'HIVE_TOKEN',
+        description: SchemaFetch.flags['registry.accessToken'].description!,
+      });
+    } catch (e) {
+      throw new MissingRegistryTokenError();
+    }
 
     const { actionId } = args;
 
@@ -172,11 +187,11 @@ export default class SchemaFetch extends Command<typeof SchemaFetch> {
     }
 
     if (schemaVersion == null) {
-      return this.error(`No schema found for action id ${actionId}`);
+      throw new SchemaNotFoundError(actionId);
     }
 
     if (schemaVersion.valid === false) {
-      return this.error(`Schema is invalid for action id ${actionId}`);
+      throw new InvalidSchemaError(actionId);
     }
 
     if (schemaVersion.schemas) {
@@ -202,7 +217,7 @@ export default class SchemaFetch extends Command<typeof SchemaFetch> {
       const schema = schemaVersion.sdl ?? schemaVersion.supergraph;
 
       if (schema == null) {
-        return this.error(`No ${sdlType} found for action id ${actionId}`);
+        throw new SchemaNotFoundError(actionId);
       }
 
       if (flags.write) {
@@ -215,8 +230,7 @@ export default class SchemaFetch extends Command<typeof SchemaFetch> {
             await writeFile(filepath, schema, 'utf8');
             break;
           default:
-            this.logFailure(`Unsupported file extension ${extname(flags.write)}`);
-            this.exit(1);
+            throw new UnsupportedFileExtensionError(flags.write);
         }
         return;
       }
diff --git a/packages/libraries/cli/src/commands/schema/publish.ts b/packages/libraries/cli/src/commands/schema/publish.ts
index 4fbcc335ac..d3951a573a 100644
--- a/packages/libraries/cli/src/commands/schema/publish.ts
+++ b/packages/libraries/cli/src/commands/schema/publish.ts
@@ -1,11 +1,22 @@
-import { existsSync, readFileSync } from 'fs';
 import { GraphQLError, print } from 'graphql';
 import { transformCommentsToDescriptions } from '@graphql-tools/utils';
 import { Args, Errors, Flags } from '@oclif/core';
 import Command from '../../base-command';
 import { DocumentType, graphql } from '../../gql';
 import { graphqlEndpoint } from '../../helpers/config';
-import { ACCESS_TOKEN_MISSING } from '../../helpers/errors';
+import {
+  APIError,
+  GithubAuthorRequiredError,
+  GithubCommitRequiredError,
+  InvalidSDLError,
+  MissingEndpointError,
+  MissingEnvironmentError,
+  MissingRegistryTokenError,
+  SchemaPublishFailedError,
+  SchemaPublishMissingServiceError,
+  SchemaPublishMissingUrlError,
+  UnexpectedError,
+} from '../../helpers/errors';
 import { gitInfo } from '../../helpers/git';
 import { loadSchema, minifySchema, renderChanges, renderErrors } from '../../helpers/schema';
 import { invariant } from '../../helpers/validation';
@@ -143,7 +154,7 @@ export default class SchemaPublish extends Command<typeof SchemaPublish> {
     }),
   };
 
-  resolveMetadata(metadata: string | undefined): string | undefined {
+  resolveMetadata = (metadata: string | undefined): string | undefined => {
     if (!metadata) {
       return;
     }
@@ -155,26 +166,9 @@ export default class SchemaPublish extends Command<typeof SchemaPublish> {
       return metadata;
     } catch (e) {
       // If we can't parse it, we can try to load it from FS
-      const exists = existsSync(metadata);
-
-      if (!exists) {
-        throw new Error(
-          `Failed to load metadata from "${metadata}": Please specify a path to an existing file, or a string with valid JSON.`,
-        );
-      }
-
-      try {
-        const fileContent = readFileSync(metadata, 'utf-8');
-        JSON.parse(fileContent);
-
-        return fileContent;
-      } catch (e) {
-        throw new Error(
-          `Failed to load metadata from file "${metadata}": Please make sure the file is readable and contains a valid JSON`,
-        );
-      }
+      return this.readJSON(metadata);
     }
-  }
+  };
 
   async run() {
     try {
@@ -182,20 +176,30 @@ export default class SchemaPublish extends Command<typeof SchemaPublish> {
 
       await this.require(flags);
 
-      const endpoint = this.ensure({
-        key: 'registry.endpoint',
-        args: flags,
-        legacyFlagName: 'registry',
-        defaultValue: graphqlEndpoint,
-        env: 'HIVE_REGISTRY',
-      });
-      const accessToken = this.ensure({
-        key: 'registry.accessToken',
-        args: flags,
-        legacyFlagName: 'token',
-        env: 'HIVE_TOKEN',
-        message: ACCESS_TOKEN_MISSING,
-      });
+      let endpoint: string, accessToken: string;
+      try {
+        endpoint = this.ensure({
+          key: 'registry.endpoint',
+          args: flags,
+          legacyFlagName: 'registry',
+          defaultValue: graphqlEndpoint,
+          env: 'HIVE_REGISTRY',
+          description: SchemaPublish.flags['registry.endpoint'].description!,
+        });
+      } catch (e) {
+        throw new MissingEndpointError();
+      }
+      try {
+        accessToken = this.ensure({
+          key: 'registry.accessToken',
+          args: flags,
+          legacyFlagName: 'token',
+          env: 'HIVE_TOKEN',
+          description: SchemaPublish.flags['registry.accessToken'].description!,
+        });
+      } catch (e) {
+        throw new MissingRegistryTokenError();
+      }
       const service = flags.service;
       const url = flags.url;
       const file = args.file;
@@ -235,18 +239,21 @@ export default class SchemaPublish extends Command<typeof SchemaPublish> {
       }
 
       if (!author) {
-        throw new Errors.CLIError(`Missing "author"`);
+        throw new GithubAuthorRequiredError();
       }
 
       if (!commit) {
-        throw new Errors.CLIError(`Missing "commit"`);
+        throw new GithubCommitRequiredError();
       }
 
       if (usesGitHubApp) {
         // eslint-disable-next-line no-process-env
         const repository = process.env['GITHUB_REPOSITORY'] ?? null;
         if (!repository) {
-          throw new Errors.CLIError(`Missing "GITHUB_REPOSITORY" environment variable.`);
+          throw new MissingEnvironmentError([
+            'GITHUB_REPOSITORY',
+            'Github repository full name, e.g. graphql-hive/console',
+          ]);
         }
         gitHub = {
           repository,
@@ -262,11 +269,7 @@ export default class SchemaPublish extends Command<typeof SchemaPublish> {
         sdl = minifySchema(transformedSDL);
       } catch (err) {
         if (err instanceof GraphQLError) {
-          const location = err.locations?.[0];
-          const locationString = location
-            ? ` at line ${location.line}, column ${location.column}`
-            : '';
-          throw new Error(`The SDL is not valid${locationString}:\n ${err.message}`);
+          throw new InvalidSDLError(err);
         }
         throw err;
       }
@@ -319,15 +322,9 @@ export default class SchemaPublish extends Command<typeof SchemaPublish> {
           this.log('Waiting for other schema publishes to complete...');
           result = null;
         } else if (result.schemaPublish.__typename === 'SchemaPublishMissingServiceError') {
-          this.logFailure(
-            `${result.schemaPublish.missingServiceError} Please use the '--service <name>' parameter.`,
-          );
-          this.exit(1);
+          throw new SchemaPublishMissingServiceError(result.schemaPublish.missingServiceError);
         } else if (result.schemaPublish.__typename === 'SchemaPublishMissingUrlError') {
-          this.logFailure(
-            `${result.schemaPublish.missingUrlError} Please use the '--url <url>' parameter.`,
-          );
-          this.exit(1);
+          throw new SchemaPublishMissingUrlError(result.schemaPublish.missingUrlError);
         } else if (result.schemaPublish.__typename === 'SchemaPublishError') {
           const changes = result.schemaPublish.changes;
           const errors = result.schemaPublish.errors;
@@ -340,8 +337,7 @@ export default class SchemaPublish extends Command<typeof SchemaPublish> {
           this.log('');
 
           if (!force) {
-            this.logFailure('Failed to publish schema');
-            this.exit(1);
+            throw new SchemaPublishFailedError();
           } else {
             this.logSuccess('Schema published (forced)');
           }
@@ -352,17 +348,19 @@ export default class SchemaPublish extends Command<typeof SchemaPublish> {
         } else if (result.schemaPublish.__typename === 'GitHubSchemaPublishSuccess') {
           this.logSuccess(result.schemaPublish.message);
         } else {
-          this.error(
-            'message' in result.schemaPublish ? result.schemaPublish.message : 'Unknown error',
+          throw new APIError(
+            'message' in result.schemaPublish
+              ? result.schemaPublish.message
+              : `Received unhandled type "${(result.schemaPublish as any)?.__typename}" in response.`,
           );
         }
       } while (result === null);
     } catch (error) {
-      if (error instanceof Errors.ExitError) {
+      if (error instanceof Errors.CLIError) {
         throw error;
       } else {
         this.logFailure('Failed to publish schema');
-        this.handleFetchError(error);
+        throw new UnexpectedError(error instanceof Error ? error.message : JSON.stringify(error));
       }
     }
   }
diff --git a/packages/libraries/cli/src/commands/whoami.ts b/packages/libraries/cli/src/commands/whoami.ts
index 05f820bd7a..ac13e44b93 100644
--- a/packages/libraries/cli/src/commands/whoami.ts
+++ b/packages/libraries/cli/src/commands/whoami.ts
@@ -2,7 +2,12 @@ import { Flags } from '@oclif/core';
 import Command from '../base-command';
 import { graphql } from '../gql';
 import { graphqlEndpoint } from '../helpers/config';
-import { ACCESS_TOKEN_MISSING } from '../helpers/errors';
+import {
+  InvalidRegistryTokenError,
+  MissingEndpointError,
+  MissingRegistryTokenError,
+  UnexpectedError,
+} from '../helpers/errors';
 import { Texture } from '../helpers/texture/texture';
 
 const myTokenInfoQuery = graphql(/* GraphQL */ `
@@ -62,29 +67,35 @@ export default class WhoAmI extends Command<typeof WhoAmI> {
 
   async run() {
     const { flags } = await this.parse(WhoAmI);
+    let registry: string, token: string;
+    try {
+      registry = this.ensure({
+        key: 'registry.endpoint',
+        legacyFlagName: 'registry',
+        args: flags,
+        defaultValue: graphqlEndpoint,
+        env: 'HIVE_REGISTRY',
+        description: WhoAmI.flags['registry.endpoint'].description!,
+      });
+    } catch (e) {
+      throw new MissingEndpointError();
+    }
 
-    const registry = this.ensure({
-      key: 'registry.endpoint',
-      legacyFlagName: 'registry',
-      args: flags,
-      defaultValue: graphqlEndpoint,
-      env: 'HIVE_REGISTRY',
-    });
-    const token = this.ensure({
-      key: 'registry.accessToken',
-      legacyFlagName: 'token',
-      args: flags,
-      env: 'HIVE_TOKEN',
-      message: ACCESS_TOKEN_MISSING,
-    });
-
-    const result = await this.registryApi(registry, token)
-      .request({
-        operation: myTokenInfoQuery,
-      })
-      .catch(error => {
-        this.handleFetchError(error);
+    try {
+      token = this.ensure({
+        key: 'registry.accessToken',
+        legacyFlagName: 'token',
+        args: flags,
+        env: 'HIVE_TOKEN',
+        description: WhoAmI.flags['registry.accessToken'].description!,
       });
+    } catch (e) {
+      throw new MissingRegistryTokenError();
+    }
+
+    const result = await this.registryApi(registry, token).request({
+      operation: myTokenInfoQuery,
+    });
 
     if (result.tokenInfo.__typename === 'TokenInfo') {
       const { tokenInfo } = result;
@@ -115,10 +126,12 @@ export default class WhoAmI extends Command<typeof WhoAmI> {
 
       this.log(print());
     } else if (result.tokenInfo.__typename === 'TokenNotFoundError') {
-      this.error(`Token not found. Reason: ${result.tokenInfo.message}`, {
-        exit: 0,
-        suggestions: [`How to create a token? https://docs.graphql-hive.com/features/tokens`],
-      });
+      this.debug(result.tokenInfo.message);
+      throw new InvalidRegistryTokenError();
+    } else {
+      throw new UnexpectedError(
+        `Token response got an unsupported type: ${(result.tokenInfo as any).__typename}`,
+      );
     }
   }
 }
diff --git a/packages/libraries/cli/src/helpers/errors.ts b/packages/libraries/cli/src/helpers/errors.ts
index 964839fe3a..a525c56ea4 100644
--- a/packages/libraries/cli/src/helpers/errors.ts
+++ b/packages/libraries/cli/src/helpers/errors.ts
@@ -1 +1,412 @@
-export const ACCESS_TOKEN_MISSING = `--registry.accessToken is required. For help generating an access token, see https://the-guild.dev/graphql/hive/docs/management/targets#registry-access-tokens`;
+import { extname } from 'node:path';
+import { env } from 'node:process';
+import { GraphQLError } from 'graphql';
+import { InvalidDocument } from '@graphql-inspector/core';
+import { CLIError } from '@oclif/core/lib/errors';
+import { CompositionFailure } from '@theguild/federation-composition';
+import { SchemaErrorConnection } from '../gql/graphql';
+import { renderErrors } from './schema';
+import { Texture } from './texture/texture';
+
+export const ACCESS_TOKEN_MISSING = '@TODO FIX';
+
+export enum ExitCode {
+  // The command execution succeeded.
+  SUCCESS = 0,
+
+  // The command execution failed with a completion code that signals an error.
+  ERROR = 1,
+
+  // The CLI was able to handle the command but it took too long and timed out.
+  TIMED_OUT = 2,
+
+  // Initialization of the CLI failed. E.g. malformed input
+  BAD_INIT = 3,
+}
+
+export class HiveCLIError extends CLIError {
+  constructor(
+    public readonly exitCode: ExitCode,
+    code: number,
+    message: string,
+  ) {
+    const tip = `> See https://the-guild.dev/graphql/hive/docs/api-reference/cli#errors for a complete list of error codes and recommended fixes.
+To disable this message set HIVE_NO_ERROR_TIP=1`;
+    super(`${message}  [${code}]${env.HIVE_NO_ERROR_TIP === '1' ? '' : `\n${tip}`}`);
+  }
+}
+
+/** Categorized by command */
+enum ErrorCategory {
+  GENERIC = 1_00,
+  SCHEMA_CHECK = 2_00,
+  SCHEMA_PUBLISH = 3_00,
+  APP_CREATE = 4_00,
+  ARTIFACT_FETCH = 5_00,
+  DEV = 6_00,
+}
+
+const errorCode = (category: ErrorCategory, id: number): number => {
+  return category + id;
+};
+
+export class InvalidConfigError extends HiveCLIError {
+  constructor(configName = 'hive.json') {
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.GENERIC, 0),
+      `The provided "${configName}" is invalid.`,
+    );
+  }
+}
+
+export class InvalidCommandError extends HiveCLIError {
+  constructor(command: string) {
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.GENERIC, 1),
+      `The command, "${command}", does not exist.`,
+    );
+  }
+}
+
+export class MissingArgumentsError extends HiveCLIError {
+  constructor(...requiredArgs: Array<[string, string]>) {
+    const argsStr = requiredArgs.map(a => `${a[0]} \t${a[1]}`).join('\n');
+    const message = `Missing ${requiredArgs.length} required argument${requiredArgs.length > 1 ? 's' : ''}:\n${argsStr}`;
+    super(ExitCode.BAD_INIT, errorCode(ErrorCategory.GENERIC, 2), message);
+  }
+}
+
+export class MissingRegistryTokenError extends HiveCLIError {
+  constructor() {
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.GENERIC, 3),
+      `A registry token is required to perform the action. For help generating an access token, see https://the-guild.dev/graphql/hive/docs/management/targets#registry-access-tokens`,
+    );
+  }
+}
+
+export class MissingCdnKeyError extends HiveCLIError {
+  constructor() {
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.GENERIC, 4),
+      `A CDN key is required to perform the action. For help generating a CDN key, see https://the-guild.dev/graphql/hive/docs/management/targets#cdn-access-tokens`,
+    );
+  }
+}
+
+export class MissingEndpointError extends HiveCLIError {
+  constructor() {
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.GENERIC, 5),
+      `A registry endpoint is required to perform the action.`,
+    );
+  }
+}
+
+export class InvalidRegistryTokenError extends HiveCLIError {
+  constructor() {
+    super(
+      ExitCode.ERROR,
+      errorCode(ErrorCategory.GENERIC, 6),
+      `A valid registry token is required to perform the action. The registry token used does not exist or has been revoked.`,
+    );
+  }
+}
+
+export class InvalidCdnKeyError extends HiveCLIError {
+  constructor() {
+    super(
+      ExitCode.ERROR,
+      errorCode(ErrorCategory.GENERIC, 7),
+      `A valid CDN key is required to perform the action. The CDN key used does not exist or has been revoked.`,
+    );
+  }
+}
+
+export class MissingCdnEndpointError extends HiveCLIError {
+  constructor() {
+    super(
+      ExitCode.ERROR,
+      errorCode(ErrorCategory.GENERIC, 8),
+      `A CDN endpoint is required to perform the action.`,
+    );
+  }
+}
+
+export class MissingEnvironmentError extends HiveCLIError {
+  constructor(...requiredVars: Array<[string, string]>) {
+    const varsStr = requiredVars.map(a => `\t${a[0]} \t${a[1]}`).join('\n');
+    const message = `Missing required environment variable${requiredVars.length > 1 ? 's' : ''}:\n${varsStr}`;
+    super(ExitCode.BAD_INIT, errorCode(ErrorCategory.GENERIC, 9), message);
+  }
+}
+
+export class SchemaFileNotFoundError extends HiveCLIError {
+  constructor(fileName: string, reason?: string | Error) {
+    const message = reason instanceof Error ? reason.message : reason;
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.SCHEMA_CHECK, 0),
+      `Error reading the schema file "${fileName}"${message ? `: ${message}` : '.'}`,
+    );
+  }
+}
+
+export class SchemaFileEmptyError extends HiveCLIError {
+  constructor(fileName: string) {
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.SCHEMA_CHECK, 1),
+      `The schema file "${fileName}" is empty.`,
+    );
+  }
+}
+
+export class GithubCommitRequiredError extends HiveCLIError {
+  constructor() {
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.GENERIC, 10),
+      `Couldn't resolve commit sha required for GitHub Application.`,
+    );
+  }
+}
+
+export class GithubRepositoryRequiredError extends HiveCLIError {
+  constructor() {
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.GENERIC, 11),
+      `Couldn't resolve git repository required for GitHub Application.`,
+    );
+  }
+}
+
+export class GithubAuthorRequiredError extends HiveCLIError {
+  constructor() {
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.GENERIC, 12),
+      `Couldn't resolve commit sha required for GitHub Application.`,
+    );
+  }
+}
+
+export class SchemaPublishFailedError extends HiveCLIError {
+  constructor() {
+    super(ExitCode.ERROR, errorCode(ErrorCategory.SCHEMA_PUBLISH, 0), `Schema publish failed.`);
+  }
+}
+
+export class HTTPError extends HiveCLIError {
+  constructor(endpoint: string, status: number, message: string) {
+    const is400 = status >= 400 && status < 500;
+    super(
+      ExitCode.ERROR,
+      errorCode(ErrorCategory.GENERIC, 13),
+      `A ${is400 ? 'client' : 'server'} error occurred while performing the action. A call to "${endpoint}" failed with Status: ${status}, Text: ${message}`,
+    );
+  }
+}
+
+export class NetworkError extends HiveCLIError {
+  constructor(cause: Error | string) {
+    super(
+      ExitCode.ERROR,
+      errorCode(ErrorCategory.GENERIC, 14),
+      `A network error occurred while performing the action: "${cause instanceof Error ? `${cause.name}: ${cause.message}` : cause}"`,
+    );
+  }
+}
+
+/** GraphQL Errors returned from an operation. Note that some GraphQL Errors that require specific steps to correct are handled through other error types. */
+export class APIError extends HiveCLIError {
+  public ref?: string;
+  constructor(cause: Error | string, requestId?: string) {
+    super(
+      ExitCode.ERROR,
+      errorCode(ErrorCategory.GENERIC, 15),
+      (cause instanceof Error ? `${cause.name}: ${cause.message}` : cause) +
+        (requestId ? `  (Request ID: "${requestId}")` : ''),
+    );
+    this.ref = requestId;
+  }
+}
+
+export class IntrospectionError extends HiveCLIError {
+  constructor() {
+    super(
+      ExitCode.ERROR,
+      errorCode(ErrorCategory.GENERIC, 16),
+      'Could not get introspection result from the service. Make sure introspection is enabled by the server.',
+    );
+  }
+}
+
+export class InvalidSDLError extends HiveCLIError {
+  constructor(err: GraphQLError) {
+    const location = err.locations?.[0];
+    const locationString = location ? ` at line ${location.line}, column ${location.column}` : '';
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.SCHEMA_PUBLISH, 1),
+      `The SDL is not valid${locationString}:\n ${err.message}`,
+    );
+  }
+}
+
+export class SchemaPublishMissingServiceError extends HiveCLIError {
+  constructor(message: string) {
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.SCHEMA_PUBLISH, 2),
+      `${message} Please use the '--service <name>' parameter.`,
+    );
+  }
+}
+
+export class SchemaPublishMissingUrlError extends HiveCLIError {
+  constructor(message: string) {
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.SCHEMA_PUBLISH, 3),
+      `${message} Please use the '--url <url>' parameter.`,
+    );
+  }
+}
+
+export class InvalidDocumentsError extends HiveCLIError {
+  constructor(invalidDocuments: InvalidDocument[]) {
+    const message = invalidDocuments
+      .map(doc => {
+        return `${Texture.failure(doc.source)}\n${doc.errors.map(e => ` - ${Texture.boldQuotedWords(e.message)}`).join('\n')}`;
+      })
+      .join('\n');
+    super(ExitCode.ERROR, errorCode(ErrorCategory.SCHEMA_CHECK, 2), message);
+  }
+}
+
+export class ServiceAndUrlLengthMismatch extends HiveCLIError {
+  constructor(services: string[], urls: string[]) {
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.DEV, 0),
+      `Not every services has a matching url. Got ${services.length} services and ${urls.length} urls.`,
+    );
+  }
+}
+
+export class LocalCompositionError extends HiveCLIError {
+  constructor(compositionResult: CompositionFailure) {
+    const message = renderErrors({
+      total: compositionResult.errors.length,
+      nodes: compositionResult.errors.map(error => ({
+        message: error.message,
+      })),
+    });
+    super(ExitCode.ERROR, errorCode(ErrorCategory.DEV, 1), `Local composition failed:\n${message}`);
+  }
+}
+
+export class RemoteCompositionError extends HiveCLIError {
+  constructor(errors: SchemaErrorConnection) {
+    const message = renderErrors(errors);
+    super(ExitCode.ERROR, errorCode(ErrorCategory.DEV, 2), message);
+  }
+}
+
+export class InvalidCompositionResultError extends HiveCLIError {
+  /** Compose API spits out the error message */
+  constructor(supergraph?: string | undefined | null) {
+    super(
+      ExitCode.ERROR,
+      errorCode(ErrorCategory.DEV, 3),
+      `Composition resulted in an invalid supergraph: ${supergraph}`,
+    );
+  }
+}
+
+export class PersistedOperationsMalformedError extends HiveCLIError {
+  constructor(file: string) {
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.APP_CREATE, 0),
+      `Persisted Operations file "${file}" is malformed.`,
+    );
+  }
+}
+
+export class UnsupportedFileExtensionError extends HiveCLIError {
+  constructor(filename: string) {
+    super(ExitCode.BAD_INIT, errorCode(ErrorCategory.GENERIC, 17), `${extname(filename)}`);
+  }
+}
+
+export class FileMissingError extends HiveCLIError {
+  constructor(fileName: string, additionalContext?: string) {
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.GENERIC, 18),
+      `Failed to load file "${fileName}"${additionalContext ? `: ${additionalContext}` : '.'}`,
+    );
+  }
+}
+
+export class InvalidFileContentsError extends HiveCLIError {
+  constructor(fileName: string, expectedFormat: string) {
+    super(
+      ExitCode.BAD_INIT,
+      errorCode(ErrorCategory.GENERIC, 19),
+      `File "${fileName}" could not be parsed. Please make sure the file is readable and contains a valid ${expectedFormat}.`,
+    );
+  }
+}
+
+export class SchemaNotFoundError extends HiveCLIError {
+  constructor(actionId?: string) {
+    super(
+      ExitCode.ERROR,
+      errorCode(ErrorCategory.ARTIFACT_FETCH, 0),
+      `No schema found${actionId ? ` for action id ${actionId}.` : '.'}`,
+    );
+  }
+}
+
+export class InvalidSchemaError extends HiveCLIError {
+  constructor(actionId?: string) {
+    super(
+      ExitCode.ERROR,
+      errorCode(ErrorCategory.ARTIFACT_FETCH, 1),
+      `Schema is invalid${actionId ? ` for action id ${actionId}.` : '.'}`,
+    );
+  }
+}
+
+export class UnexpectedError extends HiveCLIError {
+  constructor(cause: unknown) {
+    const message =
+      cause instanceof Error
+        ? cause.message
+        : typeof cause === 'string'
+          ? cause
+          : JSON.stringify(cause);
+    super(
+      ExitCode.ERROR,
+      errorCode(ErrorCategory.GENERIC, 99),
+      `An unexpected error occurred: ${message}\n> Enable DEBUG=* for more details.`,
+    );
+  }
+}
+
+export interface AggregateError extends Error {
+  errors: Error[];
+}
+
+export function isAggregateError(error: unknown): error is AggregateError {
+  return !!error && typeof error === 'object' && 'errors' in error && Array.isArray(error.errors);
+}