Exception Handling
In LittleHorse, a Failure is analogous to an Exception in Programming.
Recall from the Concepts docs that in LittleHorse, an ERROR refers to a technical issue (eg. network outage or uncaught exception from a Task Worker), and an EXCEPTION refers to a business process-level exception (eg. insufficient funds in a credit card).
Throwing an EXCEPTION
This section is concerned with throwing an EXCEPTION at the ThreadSpec level inside a WfSpec. If you want to throw an EXCEPTION at the Task Worker level, please refer to the Task Worker Development Docs
In most programming languages such as Java and Python, you can throw or raise an Exception. For example:
class MyError(Exception):
def __init__(self, foo: str):
self._foo = foo
if something_bad_happens():
raise MyError("bar")
do_something_else()
Raising a MyError here interrupts the flow of the program and prevents do_something_else() from being called. Similarly, throwing an EXCEPTION in LittleHorse can stop the flow of the ThreadRun.
Even though GoLang itself doesn't allow you to interrupt program execution with exceptions, you can still use the Go SDK to define a WfSpec that throws a LittleHorse EXCEPTION.
Let's throw an EXCEPTION with the name payment-failed. To do this, we will need the WorkflowThread#fail() method, which takes two arguments:
- The name of the exception to throw.
- A human readable error message which will show up on the
WfRun.
- Java
- Go
- Python
- C#
// Throw a normal exception
wf.fail("payment-failed", "This is a human readable error message for developers");
wf.Fail(nil, "payment-failed", "This is a human readable error message for developers")
# Throw a normal exception
wf.fail("payment-failed", "This is a human readable error message for developers")
// Throw a normal exception
wf.Fail("payment-failed", "This is a human readable error message for developers");
Note that you can specify an optional argument, which is either a WfRunVariable or some literal value that represents the content of the Exception we throw. In future versions of LittleHorse, you will be able to access this value as an input variable in the Exception Handler ThreadRun.
- Java
- Go
- Python
- C#
// Throw an exception with content
WfRunVariable exnContent = ...;
wf.fail(exnContent, "payment-failed", "This is a human readable error message for developers");
// Fail with content.
var exnContent *lhproto.WfRunVariable
// ...
wf.Fail(exnContent, "payment-failed", "This is a human readable error message for developers")
# Throw an exception with content
exn_content = ...
wf.fail("payment-failed", "This is a human readable error message for developers", exn_content)
// Throw an exception with content
WfRunVariable exnContent = ...;
wf.Fail(exnContent, "payment-failed", "This is a human readable error message for developers");
Like many things in LittleHorse, a user-defined EXCEPTION must be in sub-domain-case. For those of you who love Kubernetes, this is the same regex used by K8s resource names.
Handling a Failure.
In LittleHorse, there are two different types of Failures:
EXCEPTION, which denotes something that went wrong at the business-process level (eg. an executive rejected a transaction).ERROR, which denotes a technical failure, such as a third-party API being unavailable due to a network partition.
The WorkflowThread has three methods to allow you to handle various types of Failures:
handleException(), which handlesEXCEPTIONbusiness failures.handleError(), which handlesERRORtechnical failures.handleAnyFailure(), which catches any failure of any type.
All three methods require a NodeOutput for the Node on which to add the failureHandler. Additionally, all three methods require a ThreadFunc which defines the logic for the Failure Handler (either a lambda or a function pointer).
The syntax to handle a Failure is similar no matter which type of Node you are handling a failure for.
Handling Exceptions
Let's handle a business failure with the WorkflowThread#handleException method. You need to provide:
- The
NodeOutputto handle the failure on. - A
ThreadFunc(function pointer or lambda) to execute as the exception handler.
You can optionally provide the name of a specific EXCEPTION to handle. If that is not provided, it will handle any business EXCEPTION (but not a technical failure).
In this example, we will handle an EXCEPTION thrown by a Child ThreadRun. We catch the exception from the waitForTheads() call.
You'll notice that we have two Failure Handlers defined in the example below. The way this behaves in practice is that the first matching handler is executed. This is useful to allow you to handle different business exceptions with different exception handlers.
- Java
- Go
- Python
- C#
// ...
NodeOutput threadsResult = wf.waitForThreads(...);
wf.handleException(
threadsResult,
"my-exn", // handle only the "my-exn" exception
handler -> {
handler.execute("some-failure-handler-for-my-exn");
}
);
// The `handleException()` method with only two arguments catches all EXCEPTIONS
wf.handleException(
threadsResult,
handler -> {
handler.execute("some-other-task-in-failure-handler");
}
);
// We get here unless the Failure Handler fails.
wf.execute("another-task");
threadsResult := wf.WaitForThreads(...)
exnName := "my-exn"
wf.HandleException(
&threadsResult,
&exnName, // handle specific exception
func(handler *littlehorse.WorkflowThread) {
handler.Execute("some-task-in-my-exn-handler")
},
)
wf.HandleException(
&taskOutput,
&nil, // handle any exception
func(handler *littlehorse.WorkflowThread) {
handler.Execute("some-other-task-in-failure-handler")
},
)
// We will always get here unless the Failure Handler fails.
wf.Execute("another-task");
def entrypoint(wf: WorkflowThread) -> None:
def my_exn_handler(handler: WorkflowThread) -> None:
handler.execute("some-task-in-my-exn-handler")
def any_exn_handler(handler: WorkflowThread) -> None:
handler.execute("some-other-task-in-failure-handler")
threads_result = wf.wait_for_threads(...)
wf.handle_exception(output, my_exn_handler, exn_name="my-exn")
wf.handle_exception(output, any_exn_handler, exn_name=None)
# We will always get here unless the Failure Handler fails.
wf.execute("another-task")
NodeOutput threadsResult = wf.WaitForThreads(...);
wf.HandleException(
threadsResult,
"my-exn", // handle only the "my-exn" exception
handler =>
{
handler.Execute("some-failure-handler-for-my-exn");
}
);
// The `handleException()` method with only two arguments catches all EXCEPTIONS
wf.HandleException(
threadsResult,
handler =>
{
handler.Execute("some-other-task-in-failure-handler");
}
);
// We get here unless the Failure Handler fails.
wf.Execute("another-task");
Handling Errors
Let's handle a technical failure with the WorkflowThread#handleError method. Just as with handleException(), you need to provide:
- The
NodeOutputto handle the failure on. - A
ThreadFunc(function pointer or lambda) to execute as the exception handler.
You can optionally provide the name of a specific ERROR to handle. If that is not provided, it will handle any technical ERROR (but not a business EXCEPTION).
In this example, we will handle a TIMEOUT error from a TaskRun.
- Java
- Go
- Python
- C#
NodeOutput taskOutput = wf.execute("flaky-task");
wf.handleError(
taskOutput,
LHErrorType.TIMEOUT, // handle only TIMEOUT errors. Leave null to catch any ERROR.
handler -> {
handler.execute("some-task");
}
);
threadsResult := wf.WaitForThreads(...)
exnToHandle := littlehorse.Timeout
wf.HandleError(
&threadsResult,
&exnToHandle, // handle only TIMEOUT error. Leave nil to catch all ERROR.
func(handler *littlehorse.WorkflowThread) {
handler.Execute("some-task-in-my-exn-handler")
},
)
def entrypoint(wf: WorkflowThread) -> None:
def error_handler(handler: WorkflowThread) -> None:
handler.execute("some-task")
task_output = wf.wait_for_threads(...)
wf.handle_error(task_output, error_handler, error_type=LHErrorType.TIMEOUT)
NodeOutput taskOutput = wf.Execute("flaky-task");
wf.HandleError(
taskOutput,
LHErrorType.Timeout, // handle only TIMEOUT errors. Leave null to catch any ERROR.
handler =>
{
handler.Execute("some-task");
}
);