Skip to content

Internal Exception Handling

d-markey edited this page Jan 29, 2026 · 2 revisions

Handling exceptions across thread boundaries is a complex task. This page explains how Squadron manages errors that occur within background workers and ensures they are propagated back to the caller.

The Exception Lifecycle

When a task running in a worker fails, the following sequence of events occurs:

  1. Detection: The WorkerRunner executing the task in the background thread catches the exception using a try-catch block.
  2. Normalization: The caught error and stack trace are passed to SquadronException.from().
    • If the error is already a SquadronException, it is used as-is.
    • Otherwise, it is wrapped in a WorkerException.
  3. Serialization: The exception is converted into a WorkerResponse with an error status. The WorkerResponse.wrapInPlace() method calls the exception's serialize() method, which returns a List of platform-independent data.
  4. Transfer: The serialized list is sent back to the main thread via the platform's communication channel (e.g., SendPort.send() on Native or postMessage() on Web).
  5. Deserialization: In the main thread, the WorkerResponse is received. The unwrapInPlace() method uses the ExceptionManager to deserialize the error list.
    • The ExceptionManager looks at the first element of the list (the type identifier).
    • It invokes the corresponding registered deserializer.
    • If no deserializer is registered for that type, it defaults to creating a WorkerException indicating a deserialization failure.
  6. Propagation: The reconstructed SquadronException is thrown by the Worker's send() or stream() method, allowing the caller to catch it using standard try-catch blocks.

The ExceptionManager

The ExceptionManager is the central registry for exception deserializers.

  • Built-in Exceptions: Squadron comes with several built-in exception types (e.g., CanceledException, TimeoutException, WorkerException). These are pre-registered in the ExceptionManager.
  • Custom Exceptions: As explained in the Exception Management page, custom exceptions should inherit from WorkerException and must be registered manually.

Stack Traces

One of the challenges of multithreading is preserving stack traces. When an exception occurs in a worker:

  1. The worker captures the local stack trace.
  2. The stack trace is converted to a string during serialization.
  3. On the receiving side, SquadronException.loadStackTrace() re-creates a StackTrace object from this string. While this stack trace won't include the frames from the main thread that called the worker, it provides full visibility into what happened inside the worker's thread.

Exceptions in Streams

For streaming services, if an error occurs while the stream is being processed:

  1. The error is serialized and sent as a WorkerResponse.
  2. In the main thread, this response is added as an error event to the StreamController.
  3. The stream remains open unless cancelOnError was set to true when listening to the stream.
  4. Subsequent items or errors can still be received until the stream is closed by the worker.

The Role of WorkerRunner

The WorkerRunner is the heart of the background thread. It ensures that no exception goes unhandled. Even if a service initialization fails or a command is not found, the WorkerRunner will catch the error and send it back through the channel, preventing the background thread from crashing silently.

Clone this wiki locally