Skip to content

Errors Reference

Horsies has two error systems: runtime errors returned in TaskResult, and blocking startup errors raised during app initialization.

Runtime errors are returned via TaskResult[T, TaskError]. The TaskError.error_code field contains either a BuiltInTaskCode member or a user-defined string.

When a task reaches a terminal FAILED state with a TaskResult(err=TaskError(...)), the error_code is persisted to horsies_tasks.error_code for queryability. This column is NULL for successful tasks, non-terminal tasks, and worker-level failures that never produced a TaskResult. Access it via TaskInfo.error_code from handle.info().

Each execution attempt is also recorded in horsies_task_attempts with per-attempt error_code, error_message, and outcome. See Retrieving Results for API usage.

FieldTypeDescription
error_codeBuiltInTaskCode | str | NoneLibrary or domain error code
messagestr | NoneHuman-readable description
dataAny | NoneAdditional context (task_id, etc.)
exceptiondict[str, Any] | BaseException | NoneOriginal exception if applicable

All library-defined error codes are grouped into four families. The umbrella type alias BuiltInTaskCode is the union of all four enums. User code should handle these explicitly.

Errors from task execution, broker, and worker internals.

CodeDescriptionAuto-Retry?
UNHANDLED_EXCEPTIONUncaught exception in task code, wrapped to UNHANDLED_EXCEPTION by libraryYes, if in auto_retry_for
TASK_EXCEPTIONTask raised exception or returned invalid valueYes, if in auto_retry_for
WORKER_CRASHEDWorker process died during executionYes, if in auto_retry_for
BROKER_ERRORDatabase or broker failure during operationNo
WORKER_RESOLUTION_ERRORFailed to resolve task by nameNo
WORKER_SERIALIZATION_ERRORFailed to serialize/deserialize task dataNo
RESULT_DESERIALIZATION_ERRORStored result JSON is corrupt or could not be deserializedNo
WORKFLOW_ENQUEUE_FAILEDWorkflow node failed after READY->ENQUEUED transition during enqueue/buildNo
SUBWORKFLOW_LOAD_FAILEDSubworkflow definition could not be loadedNo

Errors from type/schema validation and structural contracts.

CodeDescriptionAuto-Retry?
RETURN_TYPE_MISMATCHTask return type doesn’t match declarationNo
PYDANTIC_HYDRATION_ERRORTask succeeded but its return value could not be rehydrated to the declared typeNo
WORKFLOW_CTX_MISSING_IDWorkflow context is missing required IDNo

Errors from handle.get() / get_async(). These indicate issues retrieving the result, not task execution failures.

CodeDescriptionAuto-Retry?
WAIT_TIMEOUTget() timed out; task may still be runningNo
TASK_NOT_FOUNDTask ID doesn’t exist in databaseNo
WORKFLOW_NOT_FOUNDWorkflow ID doesn’t exist in databaseNo
RESULT_NOT_AVAILABLEResult cache was never setNo
RESULT_NOT_READYResult not yet available; task is still runningNo

Terminal outcome codes for tasks and workflows.

CodeDescriptionAuto-Retry?
TASK_CANCELLEDTask was cancelled before completionNo
TASK_EXPIREDTask was never claimed before good_until deadline passedNo
WORKFLOW_PAUSEDWorkflow was pausedNo
WORKFLOW_FAILEDWorkflow failedNo
WORKFLOW_CANCELLEDWorkflow was cancelledNo
UPSTREAM_SKIPPEDUpstream task in workflow was skippedNo
SUBWORKFLOW_FAILEDSubworkflow failedNo
WORKFLOW_SUCCESS_CASE_NOT_METWorkflow success condition was not satisfiedNo

Send errors are returned via TaskSendResult[TaskHandle[T]] from .send(), .send_async(), and .schedule(). These are separate from TaskResult — they indicate that enqueuing the task itself failed, not that task execution failed.

FieldTypeDescription
codeTaskSendErrorCodeFailure category
messagestrHuman-readable description
retryableboolWhether the caller can retry with the same payload
task_idstr | NoneGenerated task ID (None for SEND_SUPPRESSED, VALIDATION_FAILED)
payloadTaskSendPayload | NoneSerialized envelope for idempotent retry
exceptionBaseException | NoneThe original cause, if any
CodeDescriptionRetryable
SEND_SUPPRESSEDSend suppressed during worker import/discovery to prevent side effectsNo
VALIDATION_FAILEDArgument serialization or validation failed before enqueueNo
ENQUEUE_FAILEDBroker/database failure during enqueue (transient)Yes
PAYLOAD_MISMATCHRetry payload SHA does not match — payload was altered between send and retryNo

ENQUEUE_FAILED errors carry a TaskSendPayload with an enqueue_sha field. Pass the error to .retry_send(error) or .retry_send_async(error) to replay the exact payload. The SHA guarantees same-payload idempotency.

Workflow-specific error codes are distributed across the four families above:

  • OperationalErrorCode: WORKFLOW_ENQUEUE_FAILED, SUBWORKFLOW_LOAD_FAILED
  • ContractCode: WORKFLOW_CTX_MISSING_ID
  • RetrievalCode: WORKFLOW_NOT_FOUND
  • OutcomeCode: WORKFLOW_PAUSED, WORKFLOW_FAILED, WORKFLOW_CANCELLED, UPSTREAM_SKIPPED, SUBWORKFLOW_FAILED, WORKFLOW_SUCCESS_CASE_NOT_MET

Domain-specific errors use string codes:

from horsies import TaskResult, TaskError
@app.task("validate_order")
def validate_order(order_id: str) -> TaskResult[dict, TaskError]:
order = get_order(order_id)
if order.total > 10000:
return TaskResult(err=TaskError(
error_code="ORDER_LIMIT_EXCEEDED",
message=f"Order {order_id} exceeds limit",
data={"order_id": order_id, "total": order.total},
))
return TaskResult(ok={"status": "valid"})

User-defined codes are auto-retried when listed in auto_retry_for:

@app.task(
"call_api",
retry_policy=RetryPolicy.fixed([30, 60, 120], auto_retry_for=["RATE_LIMITED", "SERVICE_UNAVAILABLE"]),
)
def call_api(url: str) -> TaskResult[dict, TaskError]:
...

Startup errors are blocking exceptions raised during app initialization, workflow validation, or task registration. If a startup error occurs, the worker or scheduler will fail to start and will not accept tasks.

These errors use ErrorCode (not BuiltInTaskCode) and indicate structural or configuration issues that must be resolved before the application can run.

Use the horsies check command to validate your configuration, task registry, and workflow definitions without starting any services. This is recommended for CI/CD pipelines to catch blocking errors before deployment.

Terminal window
horsies check myapp.instance:app

All string values used by the built-in error code families (OperationalErrorCode, ContractCode, RetrievalCode, OutcomeCode) are reserved. User-defined error codes must not collide with these values.

Runtime enforcement: TaskError(error_code="BROKER_ERROR") raises ValueError at construction time. Built-in codes must be passed as enum members. User-defined codes must be plain str, not str, Enum subclasses:

TaskError(error_code=OperationalErrorCode.BROKER_ERROR) # correct
TaskError(error_code="MY_CUSTOM_CODE") # correct — user string
TaskError(error_code="BROKER_ERROR") # ValueError — reserved

Serialization: built-in codes are serialized as tagged dicts {"__builtin_task_code__": "BROKER_ERROR"} so round-trip identity is preserved through standard model_validate() / model_validate_json(). User strings are serialized as plain strings. This tagged format eliminates wire-format ambiguity between built-in and user codes, allowing model_validate() to work correctly everywhere — including nested models.

horsies check detects reserved-code collisions in statically visible configuration:

  • exception_mapper values — if any mapped error code string matches a reserved built-in code, horsies check reports HRS-212.
  • default_unhandled_error_code — if set to a reserved built-in code other than the library default UNHANDLED_EXCEPTION, horsies check reports HRS-212.

The library default UNHANDLED_EXCEPTION is intentionally a built-in code and is not flagged.

Example collision that horsies check catches:

app = Horsies(
config=AppConfig(
broker=PostgresConfig(database_url='postgresql+psycopg://...'),
# HRS-212: 'BROKER_ERROR' collides with OperationalErrorCode.BROKER_ERROR
exception_mapper={ValueError: 'BROKER_ERROR'},
),
)

Use a custom string that does not match any built-in code:

exception_mapper={ValueError: 'VALIDATION_FAILED'} # OK — not reserved

The full list of reserved strings is available at runtime via BUILTIN_CODE_REGISTRY from horsies.core.models.tasks.

RangeCategoryDescription
HRS-001-HRS-099Workflow ValidationInvalid workflow specification
HRS-100-HRS-199Task DefinitionInvalid task decorator usage
HRS-200-HRS-299ConfigurationInvalid app/broker configuration
HRS-300-HRS-399RegistryTask registration failures
CodeNameDescription
HRS-001WORKFLOW_NO_NAMEWorkflow has no name
HRS-002WORKFLOW_NO_NODESWorkflow has no nodes
HRS-003WORKFLOW_INVALID_NODE_IDInvalid node ID reference
HRS-004WORKFLOW_DUPLICATE_NODE_IDDuplicate node ID
HRS-005WORKFLOW_NO_ROOT_TASKSNo root tasks in workflow
HRS-006WORKFLOW_INVALID_DEPENDENCYInvalid dependency reference
HRS-007WORKFLOW_CYCLE_DETECTEDCycle detected in workflow DAG
HRS-008WORKFLOW_INVALID_ARGS_FROMInvalid args_from reference
HRS-009WORKFLOW_INVALID_CTX_FROMInvalid ctx_from reference
HRS-010WORKFLOW_CTX_PARAM_MISSINGContext parameter missing
HRS-011WORKFLOW_INVALID_OUTPUTInvalid output specification
HRS-012WORKFLOW_INVALID_SUCCESS_POLICYInvalid success policy
HRS-013WORKFLOW_INVALID_JOINInvalid join configuration
HRS-014WORKFLOW_UNRESOLVED_QUEUEQueue name not resolved
HRS-015WORKFLOW_UNRESOLVED_PRIORITYPriority not resolved
HRS-016WORKFLOW_NO_DEFINITION_KEYWorkflow definition/spec missing definition_key
HRS-017WORKFLOW_DUPLICATE_DEFINITION_KEYTwo definitions share the same definition_key
HRS-018WORKFLOW_SUBWORKFLOW_APP_MISSINGSubworkflow app reference missing
HRS-019WORKFLOW_INVALID_KWARG_KEYUnknown kwargs or args_from key for callable
HRS-020WORKFLOW_MISSING_REQUIRED_PARAMSMissing required parameters for task or subworkflow
HRS-021WORKFLOW_KWARGS_ARGS_FROM_OVERLAPkwargs and args_from share one or more keys
HRS-022WORKFLOW_SUBWORKFLOW_PARAMS_REQUIRE_BUILD_WITHSubworkflow params passed but build_with is not overridden
HRS-023WORKFLOW_SUBWORKFLOW_BUILD_WITH_BINDINGSubworkflow build_with binding error (duplicate param binding)
HRS-024WORKFLOW_ARGS_FROM_TYPE_MISMATCHargs_from source result type doesn’t match target parameter
HRS-025WORKFLOW_OUTPUT_TYPE_MISMATCHOutput node type doesn’t match WorkflowSpec generic
HRS-026WORKFLOW_POSITIONAL_ARGS_NOT_SUPPORTEDPositional args are not supported for workflow nodes
HRS-027WORKFLOW_CHECK_CASES_REQUIREDParameterized workflow builder missing test cases
HRS-028WORKFLOW_CHECK_CASE_INVALIDWorkflow builder test case is invalid
HRS-029WORKFLOW_CHECK_BUILDER_EXCEPTIONWorkflow builder raised an exception or returned non-WorkflowSpec
HRS-030WORKFLOW_CHECK_UNDECORATED_BUILDERFunction returns WorkflowSpec but lacks @app.workflow_builder
HRS-031WORKFLOW_KWARGS_NOT_SERIALIZABLEkwargs value fails JSON serialization
CodeNameDescription
HRS-100TASK_NO_RETURN_TYPETask function missing return type
HRS-101TASK_INVALID_RETURN_TYPEReturn type is not TaskResult
HRS-102TASK_INVALID_OPTIONSInvalid task options
HRS-103TASK_INVALID_QUEUEInvalid queue specification
HRS-104TASK_PREDECORATED_NOT_SUPPORTEDTask function is already decorated by another app instance
CodeNameDescription
HRS-200CONFIG_INVALID_QUEUE_MODEInvalid queue mode
HRS-201CONFIG_INVALID_CLUSTER_CAPInvalid cluster capacity
HRS-202CONFIG_INVALID_PREFETCHInvalid prefetch setting
HRS-203BROKER_INVALID_URLInvalid broker URL
HRS-204CONFIG_INVALID_RECOVERYInvalid recovery configuration
HRS-205CONFIG_INVALID_SCHEDULEInvalid schedule configuration
HRS-206CLI_INVALID_ARGSInvalid CLI arguments
HRS-207WORKER_INVALID_LOCATORInvalid worker locator
HRS-208CONFIG_INVALID_RESILIENCEInvalid resilience configuration
HRS-209CONFIG_INVALID_EXCEPTION_MAPPERInvalid exception mapper
HRS-210MODULE_EXEC_ERRORModule raised an error during import
HRS-211BROKER_INIT_FAILEDBroker failed to initialize
HRS-212CHECK_RESERVED_CODE_COLLISIONUser config value collides with a reserved built-in error code
CodeNameDescription
HRS-300TASK_NOT_REGISTEREDTask not found in registry
HRS-301TASK_DUPLICATE_NAMEDuplicate task name

Startup errors provide Rust-style formatting automatically. When raised, they display with source location and context:

error[HRS-007]: cycle detected in workflow DAG
--> /app/workflows/order.py:12
|
12 | node_b = TaskNode(fn=process, waits_for=[node_a])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: workflows must be acyclic directed graphs (DAG)
= help:
remove circular dependencies between tasks

Access fields programmatically when catching the exception:

from horsies.core.errors import HorsiesError
try:
spec = app.workflow(
name="order_flow",
tasks=[node_a, node_b],
definition_key="myapp.order_flow.v1",
)
except HorsiesError as e:
e.code # ErrorCode.WORKFLOW_CYCLE_DETECTED
e.message # "cycle detected in workflow DAG"
e.location # SourceLocation(file="/app/workflows/order.py", line=12)
e.notes # ["workflows must be acyclic directed graphs (DAG)"]
e.help_text # "remove circular dependencies between tasks"
Property/MethodTypeDescription
is_ok()boolTrue if success
is_err()boolTrue if error
okT | NoneSuccess value or None
errTaskError | NoneError value or None
ok_valueTSuccess value; raises ValueError if error
err_valueTaskErrorError value; raises ValueError if success