Skip to content

Error handling

GGCommons reports failure through each language’s normal error channel: Java throws exceptions, Python raises exceptions, Rust returns Result<T, GgError>, and TypeScript throws an Error. This guide shows the error and exception types a component developer actually encounters, organized by the scenario that produces them — startup and config validation, messaging, credentials, parameters, and streaming — with idiomatic handling in each SDK.

The four SDKs are deliberate mirrors, but their error shapes are idiomatic to each language, so the handling differs:

  • Java — one exception class per subsystem. Some are checked (ConfigurationException, ConfigurationValidationException) and the compiler forces you to handle them; the rest are unchecked (extends RuntimeException).
  • Python — one Exception subclass per subsystem (CredentialError, ParameterError, GgStreamError, ConfigurationValidationException). No checked/unchecked distinction.
  • Rust — a single library-wide enum GgError with one variant per subsystem, returned inside the alias ggcommons::Result<T>. You match on the variant or propagate with ?.
  • TypeScript — a mix: a single tagged GgError (with a kind field) covers the core subsystems, while credentials, parameters, and streaming throw their own standalone Error subclasses. See Cross-language differences.

The table below maps each failure scenario to the type you catch, per language. Generic types are in backticks to keep them readable.

Scenario Java Python Rust (GgError variant) TypeScript
Config load ConfigurationException (checked) ValueError / propagated stdlib GgError::Config GgError kind "Config"
Config validation ConfigurationValidationException (checked) ConfigurationValidationException GgError::Validation GgError kind "Validation"
CLI / bad arguments IllegalArgumentException ValueError GgError::Cli GgError kind "Cli"
Messaging RuntimeException / IllegalStateException RuntimeError / TimeoutError GgError::Messaging, GgError::Ipc GgError kind "Messaging" / "Ipc"
Credentials CredentialException CredentialError GgError::Credentials CredentialError (standalone)
Parameters ParameterException ParameterError GgError::Parameters ParameterError (standalone)
Streaming GgStreamException (has code()) GgStreamError (has .code) GgError::Streaming (code discarded) GgStreamError (standalone, has .code)
Accessor used before init (n/a) RuntimeError (n/a — typed accessors) (n/a)

Configuration is loaded and then validated against the canonical JSON schema. In Java these are two checked exceptions: loading via ConfigManagerFactory.create(componentName, cmdLine) throws ConfigurationException when no source resolves, and ConfigurationValidator.validate(config) throws ConfigurationValidationException on a schema violation. Validation is fail-closed in every language: if the bundled schema resource is missing, validation throws rather than silently passing.

import com.mbreissi.ggcommons.config.ConfigurationValidator;
import com.mbreissi.ggcommons.config.ConfigurationValidator.ConfigurationValidationException;
import com.google.gson.JsonObject;
// ConfigurationValidationException is CHECKED — handle it or declare `throws`.
try {
ConfigurationValidator.validate(config); // config is the parsed JsonObject
} catch (ConfigurationValidationException e) {
// e.getMessage() lists each schema violation.
logger.error("Configuration failed schema validation: {}", e.getMessage());
return;
}
// Config LOADING (ConfigManagerFactory.create) throws the sibling checked
// ConfigurationException when no source resolves ("No configuration found").

Messaging failures (failure to connect to the local broker or AWS IoT Core, a publish/subscribe error, or a request timeout) reach you through the messaging accessor and its operations. Rust and TypeScript have first-class messaging error signals; Java and Python surface them as standard runtime exceptions.

// Java has no dedicated messaging exception type. Connect/publish failures
// surface as RuntimeException (e.g. "Failed to connect to IoT Core ...") or
// IllegalStateException.
try {
gg.getMessaging().publish(topic, message);
} catch (RuntimeException e) {
logger.error("Publish failed", e);
}

The credentials subsystem is opt-in: the accessor returns a “missing” value (null / None / Option::None / undefined) unless a credentials config section is present — always check that first. Once you have the service, a missing secret returns an empty value, while a vault open/decrypt failure (wrong key, tampered file, corrupt or unsupported vault format) raises the credentials error. Error messages never include secret or key material.

import com.mbreissi.ggcommons.credentials.CredentialException;
import com.mbreissi.ggcommons.credentials.CredentialService;
import com.mbreissi.ggcommons.credentials.Secret;
import java.util.Optional;
CredentialService creds = gg.getCredentials(); // null if no `credentials` section
if (creds == null) {
logger.info("Credentials subsystem not configured");
return;
}
try {
Optional<Secret> secret = creds.get("db-password"); // empty if name absent
secret.ifPresent(s -> connect(s));
} catch (CredentialException e) {
// Vault open/decrypt failure (unchecked). No secret/key material in the message.
logger.error("Credential access failed: {}", e.getMessage());
}

Parameters mirror credentials: the accessor returns a missing value unless a parameters config section is present, a missing key returns empty, and a lookup/refresh failure raises the parameters error (ParameterException / ParameterError / GgError::Parameters).

import com.mbreissi.ggcommons.parameters.ParameterException;
import com.mbreissi.ggcommons.parameters.ParameterService;
import java.util.Optional;
ParameterService params = gg.getParameters(); // null if no `parameters` section
if (params == null) return;
try {
Optional<String> endpoint = params.get("api-endpoint"); // empty if absent
endpoint.ifPresent(this::configure);
} catch (ParameterException e) {
logger.error("Parameter lookup failed: {}", e.getMessage());
}

Streaming errors come from the embedded native ggstreamlog engine and carry a numeric status code (ERR_FULL, ERR_UNKNOWN_STREAM, and so on). How much of that code survives differs by language: Java exposes code() plus named constants, Python and TypeScript expose a .code integer but no named constants, and Rust discards the code entirely — it stringifies into GgError::Streaming, so a Rust caller cannot branch on the status value.

import com.mbreissi.ggcommons.streaming.GgStreamException;
import com.mbreissi.ggcommons.streaming.StreamHandle;
import com.mbreissi.ggcommons.streaming.StreamService;
StreamService streams = gg.getStreams(); // null if no `streaming` section
if (streams == null) return;
try {
StreamHandle handle = streams.stream("telemetry");
handle.append(partitionKey, System.currentTimeMillis(), payload);
} catch (GgStreamException e) {
// Branch on the native numeric status via the named constants.
if (e.code() == GgStreamException.ERR_FULL) {
logger.warn("Telemetry buffer full — applying backpressure");
} else if (e.code() == GgStreamException.ERR_UNKNOWN_STREAM) {
logger.error("No such stream configured");
} else {
logger.error("Stream append failed (code {})", e.code(), e);
}
}

Check optional subsystems before you use them

Section titled “Check optional subsystems before you use them”

Credentials, parameters, and streaming are opt-in. Their accessors return a “missing” value when the matching config section is absent — guard for it before calling methods. Messaging and metrics are always-on subsystems, but Rust and TypeScript still signal an unavailable service rather than handing back a usable object.

Accessor when the config section is absent Java Python Rust TypeScript
Credentials getCredentials()null get_credentials()None credentials()Option None credentials()undefined
Parameters getParameters()null get_parameters()None parameters()Option None parameters()undefined
Streaming getStreams()null get_streams()None streams() is compile-time gated by the streaming feature and returns the service directly (no Option) streams()undefined
Messaging getMessaging() (present) get_messaging() (present) messaging()Result (Err(GgError::Messaging) if unwired) messaging() throws GgError.messaging(...)
Metrics getMetrics() (present) get_metrics() (present) metrics() (present) metrics() throws if unavailable

The library reports failures by throwing, raising, or returning an error — it does not terminate your process for you. There are exactly two intentional exits, and they are about lifecycle, not errors:

  • Java calls System.exit(0) only on the -h/--help usage path, after printing help. Its SIGTERM/shutdown hook deliberately does not call System.exit — the JVM exits 0 once the hooks finish.
  • Python’s GGCommons orchestrator calls sys.exit(0) from its SIGTERM handler.
  • Rust and TypeScript never exit the process from the library.

Net effect on SIGTERM: Python exits the process, while Java does not. Whatever the language, don’t rely on the library to terminate on an error — catch it and decide. (And always unsubscribe and handle SIGTERM before exiting, or a run leaks subscriptions.)

A few shape differences are worth internalizing so a snippet from one language doesn’t mislead you in another:

  • One enum vs many classes. Rust folds everything into a single GgError enum, including its Streaming, Credentials, and Parameters variants. TypeScript does not: its GgErrorKind union covers only "Cli" | "Config" | "Validation" | "Messaging" | "Metrics" | "Ipc" | "Io" | "Json", and credentials/parameters/streaming are separate Error subclasses (CredentialError, ParameterError, GgStreamError). Java and Python use a distinct exception class per subsystem throughout. There is no single error type shared across all four languages.
  • Checked vs unchecked is Java-only. ConfigurationException and ConfigurationValidationException are checked (the compiler forces handling); CredentialException, ParameterException, and GgStreamException are unchecked. Python and TypeScript have no such distinction, and Rust encodes fallibility in the Result return type.
  • Streaming status code. Java keeps it (code() + named constants), Python and TypeScript keep the integer (no named constants), Rust drops it.