Retries

Prevent Retries

It is recommended to enable retries for workflow runs to improve reliability.

However, in some cases, you may want to stop execution immediately when an error occurs, without causing additional retries. Upstash Workflow provides several mechanisms to terminate workflow execution gracefully.

Using WorkflowNonRetryableError

WorkflowNonRetryableError lets you explicitly fail a workflow without entering the retry cycle.

When thrown, the workflow run is marked as failed, which:

  • Triggers the failure function (if defined)
  • Sends the workflow run to the DLQ
TypeScript
export const { POST } = serve<{ topic: string }>(async (context) => {  const payload = context.requestPayload  const isExists = await context.run("is-user-exists", () => { ... });  if (!isExists) {    throw new WorkflowNonRetryableError("The user does not exists!")  }})

Using context.cancel()

You can cancel a workflow run explicitly from inside the workflow.

When canceled, the run is labeled as canceled instead of failed. This means:

  • The failure handler will NOT be triggered
  • The workflow will NOT be sent to the DLQ
TypeScript
export const { POST } = serve<{ orderId: string }>(async (context) => {  const { orderId } = context.requestPayload;  // Check if order is still valid  const orderStatus = await context.run("check-order-status", async () => {    return await getOrderStatus(orderId);  });  if (orderStatus === "cancelled") {    // Stop execution gracefully without error    await context.cancel();    return;  }  // Continue processing if order is valid  await context.run("process-order", async () => {    return await processOrder(orderId);  });});
Python
@serve.post("/graceful-cancellation")async def graceful_cancellation(context: AsyncWorkflowContext[dict]) -> None:    order_id = context.request_payload["order_id"]    async def _check_order_status():        return await get_order_status(order_id)    # Check if order is still valid    order_status = await context.run("check-order-status", _check_order_status)    if order_status == "cancelled":        # Stop execution gracefully without error        await context.cancel()        return    # Continue processing if order is valid    async def _process_order():        return await process_order(order_id)    await context.run("process-order", _process_order)

Using conditional execution

You can also use guard conditions to skip certain steps and exit early, without throwing errors or canceling the workflow.

In this case, the workflow run completes successfully because no error was raised.

TypeScript
export const { POST } = serve<{ data: any }>(async (context) => {  const { data } = context.requestPayload;  // Check if order is still valid  const orderStatus = await context.run("check-order-status", async () => {    return await getOrderStatus(orderId);  });  if (orderStatus === "not-found") {    // Stop execution without error    return;  }  // Continue processing if order is valid  await context.run("process-order", async () => {    return await processOrder(orderId);  });});
Python
@serve.post("/conditional-execution")async def conditional_execution(context: AsyncWorkflowContext[dict]) -> None:    data = context.request_payload["data"]    async def _validate_data():        return validate_input_data(data)    # Validate data first    validation_result = await context.run("validate-data", _validate_data)    if not validation_result["is_valid"]:        # Log the validation failure        async def _log_validation_failure():            await log_validation_error(validation_result["errors"])        await context.run("log-validation-failure", _log_validation_failure)        # Stop execution without error        return    # Only execute if validation passes    async def _process_valid_data():        return await process_data(data)    await context.run("process-valid-data", _process_valid_data)
Loading search…