Skip to main content
When your code fails, Restate automatically retries the invocation. Understanding when to stop retrying is critical to building reliable applications.
Check out the Error Handling guide to learn more about how Restate handles transient errors, terminal errors, retries, and timeouts.

Understanding Retryable vs Terminal Errors

Retryable errors are temporary problems that might succeed if retried:
  • Database connection timeout
  • Network issues
  • Temporary service unavailability
Terminal errors are permanent failures that won’t be fixed by retrying:
  • Invalid user input (wrong format, missing required fields)
  • Authorization failures (user doesn’t have permission)
  • Business logic violations (insufficient balance, duplicate order)
By default, Restate retries ALL errors, except TerminalErrors.

Raising TerminalError

Use TerminalError to signal permanent failures. When you raise TerminalError in your handler code, it stops the invocation and marks it as permanently failed:
When you raise TerminalError inside a ctx.run block, it fails that specific step, allowing your handler to continue and handle the error:
Inside ctx.run, all errors are retried unless you raise TerminalError explicitly or set up a run retry policy. Common use cases for terminal error are:
  • Input validation failures: raise TerminalError("Invalid email format")
  • Business rule violations: raise TerminalError("Insufficient balance")
  • Resource not found: raise TerminalError("User ID not found")
When you throw a terminal error, you may need to undo previous actions to keep your system consistent. Check out our sagas guide to learn about compensations.

Handling Errors

Most of the time, you only need to catch TerminalError to handle permanent failures.
DO NOT use bare except: or except Exception: in Restate handlers! This will catch internal SDK exceptions that you must not handle, and which resulting action can lead to unexpected behavior.
Wrong - This leads to unexpected behavior:
Correct - Only catch what you need:
Only use finally blocks if you’re managing resources (files, connections, locks) that must be released even when retries happen.
When using finally, understand what goes where:
You can alternatively use restate.is_internal_exception(e) to identify whether an exception is a Restate SDK internal exception that should be ignored.Key principle:
  • except TerminalError: For compensations and business logic when the invocation permanently fails
  • finally: For releasing resources (files, connections) acquired during the current attempt

Retryable errors with custom delay

Use RetryableError to signal that Restate should retry the invocation with a specific delay. This is useful when you receive a Retry-After header from an external API or want to control the retry timing explicitly.
RetryableError accepts the following parameters:
  • message: The error message
  • status_code: HTTP status code (default: 500)
  • retry_after: A timedelta specifying when Restate should retry (optional)
Inside a ctx.run block, you can combine RetryableError with RunOptions to control both the retry delay and the maximum number of retries:
Unlike TerminalError which stops retries permanently, RetryableError tells Restate to retry after the specified delay. If no retry_after is provided, Restate uses its default retry policy.

Retry strategies

By default, Restate does infinite retries with an exponential backoff strategy. Check out the error handling guide to learn how to customize this.