Declaring a failure
On this page
A failure is a way an action or query can fail that its caller is
expected to handle, such as stock running out. You declare it as a type
whose base is Failure, and raise it with reject. Save this as
main.jadpo and run jadpo test main.jadpo:
/// There isn't enough stock to fill the order.type OutOfStock = Failure { kind: FailureKind.conflict message: "There isn't enough stock."}type Stock = Int { min: 0}/// The stock left once one item is sold.function after_sale(stock: Stock) fails OutOfStock -> Stock { if (stock == 0) { reject OutOfStock } return Stock(stock - 1)}test "the last one sells, and then there are none" { var left: Stock = attempt after_sale(1) assert left == 0 assert after_sale(0) fails OutOfStock}
Failure, FailureKind and Int are built in. OutOfStock, Stock and
after_sale are defined here. fails OutOfStock says the function can fail
this way, and reject raises the failure.
The /// line above OutOfStock is a documentation comment. It is
optional, and becomes the failure's description in generated documentation
and OpenAPI.
The base, Failure, gives the type its settings: kind, message,
client, internal and, rarely, code.
kind:, which sets the status
kind: names one of the failure kinds, the categories a failure falls
into, such as not_found or conflict. The kind sets the HTTP status a
client gets, and how background work, such as a
job or an event subscriber, treats the failure. A failure never states a status itself.
The code clients see
A failure's public code is its name in lower_snake_case: TodoNotFound
is todo_not_found. Clients rely on it, so it stays the same. Write
code: "todo_missing" only to keep a code clients already use after you
rename the failure.
message:
message: is fixed text that is safe to show anyone. It can't include
values. Leave it out and the client gets the generic message Jadpo keeps
for the failure's kind.
client: and internal: fields
A failure can carry values that say what went wrong, such as which product ran out. These are its fields, and they go in one of two blocks:
client: { }holds the fields sent to the caller.internal: { }holds the fields that never leave the server. They are for your logs and traces.
Here are two failures with fields. Todo and Product are entities, kinds
of stored record, declared with Quantity in another file of the app. A
field typed Todo.id has the type of Todo's id field.
/// The todo does not exist.type TodoNotFound = Failure { kind: FailureKind.not_found message: "Todo not found." internal: { todo_id: Todo.id }}type QuantityUnavailable = Failure { kind: FailureKind.conflict message: "The requested quantity is unavailable." client: { available: Quantity } internal: { product_id: Product.id requested: Quantity }}
A field name can't be in both. A failure has no fields outside these two
blocks. The caller gets a JSON body with code, message, request_id
and, when the failure has client fields, those fields under details
(How failures reach the client).
Leave client out and the body has only the code, the message and the
request id.
These fields are ordinary fields. A T? type, which may hold no
value, is allowed. optional and default are not, because only Input
types may have them (One form for every type).
Where the file goes
An app's code is split into domains: one business area each, such as
planning, in a folder under domains/
(Project layout). Each
failure has its own file in its domain's failures/ folder, such as
domains/planning/failures/todo_not_found.jadpo.
When the actions of two or more domains can fail with the same failure,
you put its file in shared/failures/ instead. When code in another
domain handles the failure, for example in a
match, write public before the
declaration: public type QuantityUnavailable = Failure { ... }.
Markdown for agents: /docs/declaring-a-failure.md.Built from Jadpo main at aee2e8f.