Osy#betaa language · its runtime Osyrin · a hosted platform
Why Osy#Built for agentsAgents as declarationsWorkflows that waitRuns exactly onceSecure by defaultNothing to mockThe editor is the compilerUI in the languageDocuments are dataOne program

Reference / Function

throw

throw new <ExceptionType>("<message>");

Raises a fault. It ends the function immediately, and the rows the function wrote are discarded rather than half-written. The exception types are a closed set you do not add to — usable as ordinary types too, so a variable, a field or a parameter can hold one. CATCHING one is different: the refused row stays staged, and the catch has to say what happens to it.

stable7 examples compiled by CIfunctionerrors

Summary#

throw raises a fault. The function stops where it stands, and the rows it wrote are discarded — a function that throws leaves nothing half-written behind it.

You throw one of a closed set of types. There is no class MyException to declare:

entity Order {
  [Required, Unique, MaxLength(20)] string Code;
  decimal Total;

  // Every entity states who may touch it — with no `security { }` block it is denied to everyone.
  // A real app scopes these grants to a user; an example still has to declare them, because an example
  // that could not actually run is not an example. See the security guide.
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

void Refund(string code, decimal amount) {
  var order = Order.SingleOrDefault(o => o.Code == code);
  if (order == null) { throw new NotFoundException($"no order '{code}'"); }
  if (amount > order.Total) { throw new ValidationException("a refund cannot exceed the order total"); }

  order.Total = order.Total - amount;
}

Signature#

throw new Exception("<message>");                  // the base type — the catch-all
throw new NotFoundException("<message>");          // asked for a thing that is not there
throw new ValidationException("<message>");        // the input or the resulting row is not acceptable
throw new ConflictException("<message>");          // it clashes with the current state of the data
throw new OAuthConnectionFailedException("<msg>"); // an external authorization handshake failed

Description#

Which types are there?#

The vocabulary is closed. Naming an unknown type is a compile error that lists every one you may use and what raises it, so you cannot misspell your way into a silent catch-all:

TypeWhat it meansAlso raised for you by
Exceptionthe base. Every other type is one, so catch (Exception e) catches everything
NotFoundExceptionyou asked for something that does not exist
ValidationExceptionthe input, or the row you are about to write, is not acceptablea broken [[entity-constraints|constraint]] or [[entity-invariants|invariant]] — including a duplicate under [Unique]
ConflictExceptionit cannot be done given the current state of the datatwo writers colliding — see ConcurrencyCheck
OAuthConnectionFailedExceptionan external authorization handshake failedthe OAuth surface
NotAuthorizedthe caller may not do this — and the one to throw when your own function refuses on authorization groundsa workflow event's [Authorize], or a slot's candidate gate
RequirementsNotMetthe WORK is not finished, which is not the same as not being alloweda slot deposit whose Requires criteria are unmet
DeviceUnavailableExceptionthe browser would not hand over a camera or microphone. ex.Message says which of the four reasonsCamera.Start() / Mic.Start()
WorkflowErroran awaited child workflow reached a terminal error. Catch-only — throwing it is refusedthe engine
WorkflowCancelledan awaited child workflow reached a terminal cancel. Catch-only — throwing it is refusedthe engine

The point of the closed set is that a catch has a finite, knowable set of things to match on, and every type the platform raises on your behalf is in it. That is what lets you catch a constraint violation as an ordinary ValidationException (try / catch / finally) rather than by inspecting a database error.

ValidationException is the duplicate one, however much English disagrees. A row refused by [Unique] is the entity's own declared rule saying no, so it is a ValidationException — not a ConflictException, which is about what the DATA currently says. The two catch differently and the English pulls the wrong way, which is why the compile error that lists these names lists what raises each beside it.

Holding one in a variable#

A fault type is an ordinary type: it may stand wherever a type name may, so a local, a field, a parameter, a return type or a generic argument can hold one. Assignment follows the same rule a typed catch does — every subtype widens to Exception, and nothing narrows back without you saying which you meant.

string Latest(string code) {
  Exception? caught = null;
  try { throw new ConflictException($"'{code}' moved under you"); }
  catch (Exception e) { caught = e; }
  return caught == null ? "fine" : $"{caught.Type}: {caught.Message}";
}

Catching a constraint violation is not the end of it#

The row the database refused is still there when your catch runs, and doing nothing about it is the one shape that does not work. This is the trap, because the code that falls into it is the code everybody writes first:

int Book(string code) {
  try {
    var b = new Order { Code = code, Total = 10m };
    return 201;
  } catch (ValidationException e) {
    return 409;              // ⛔ the catch DOES run. The refused Order is still staged, so the function
  }                          //    then faults at its own boundary with the same violation, after this
}                            //    `return` has already happened. The caller gets the platform's message.

Nothing is dropped for you, and that is deliberate: it is your data. A form that is still open on screen must be able to take the 409, let the user change the value, and save again — so the platform holds the row and leaves the decision to the app, because only the app knows whether its form closed as part of saving.

There are three things you can do about it, and which one is right is a question about your UI:

Your form…Do thisWhy
stays open after a failed saveamend the row in the catchthe end settlement persists the repair — one write, the value the user fixed
stays open, and the app should say so in its own wordsthrow new ConflictException("…")a throw discards ([[#rollback]]); your sentence reaches the caller and nothing is written
closes as part of save — there is nothing to get back toUnitOfWork.Discard() first, then returnthe row is gone, and the function returns your status code normally
// The form stays open: fix the value the user gave and let the settlement write it.
string AmendAndKeep(string code) {
  var o = new Order { Code = "provisional", Total = 10m };
  try {
    o.Code = code;
    return "written";
  } catch (ValidationException e) {
    o.Code = "provisional-2";
    return "amended";
  }
}

// Say it in the app's own words. Nothing is written.
string RefuseInMyOwnWords(string code) {
  try {
    var o = new Order { Code = code, Total = 10m };
    return "created";
  } catch (ValidationException e) {
    throw new ConflictException($"the code '{code}' is already spoken for");
  }
}

// The form closed on save. Decline the row explicitly, then answer for yourself.
int DeclineTheRow(string code) {
  try {
    var o = new Order { Code = code, Total = 10m };
    return 201;
  } catch (ValidationException e) {
    UnitOfWork.Discard();
    return 409;
  }
}

Proved, not asserted — the discard shape returns its own status code, and writes nothing:

[Test]
void Discarding_in_the_catch_lets_the_function_answer_for_itself() {
  var tooLong = "0123456789012345678901234567890123456789";   // > MaxLength(20)

  Assert.Equal(409, DeclineTheRow(tooLong));   // the function's OWN status code, not the platform's fault
  Assert.Empty(Order.ToList());                // and nothing was written
}

osy lint finds the fourth shape for you — the catch that repairs nothing, rethrows nothing and discards nothing — as data-caught-write-fault-left-staged, and its remedy names all three of the above.

Why you cannot declare your own: a fault's type is what a catch matches on, and its message is what a human reads. A bespoke type would carry no more information than the message already does, and it would not survive leaving the app — see below.

A throw discards what the function wrote#

This is the part worth internalising. A function is transactional (function), and a throw is the abort:

void PlaceTwo(string first, string second) {
  var a = new Order { Code = first, Total = 10m };

  if (second == "") { throw new ValidationException("the second code is required"); }

  var b = new Order { Code = second, Total = 20m };
}

If second is empty, neither order exists. The first new Order was already written in the ordinary sense — it is simply never committed. You do not unwind it, and there is no state in which the caller can observe it.

Proved, not asserted:

[Test]
void A_throw_discards_the_rows_the_function_had_written() {
  Assert.Throws<ValidationException>(() => PlaceTwo("A1", ""));

  Assert.Empty(Order.ToList());   // NOT one row — the first `new Order` went with it
}

[Test]
void The_type_is_what_a_caller_matches_on() {
  Assert.Throws<NotFoundException>(() => Refund("nope", 1m));
}

throw ends a path#

A throw satisfies the compiler's "all paths return a value" rule, exactly as it does in C#. A guard clause that throws needs no else:

decimal TotalOf(string code) {
  var order = Order.SingleOrDefault(o => o.Code == code);
  if (order == null) { throw new NotFoundException($"no order '{code}'"); }

  return order.Total;   // reachable only when the order exists — no `else`, no null check
}

What a caller outside the app sees#

Inside the app, a throw is caught by type (try / catch / finally).

A function reached from outside the app — over its published REST surface, or as a tool call — is different: a fault becomes a failure carrying your message, and the type does not survive the crossing. So write the message for the person who will read it, and do not expect an external caller to branch on NotFoundException versus ValidationException. Inside, the type is everything; at the edge, the message is.

See also#

  • try / catch / finally — catching one, and what else can throw
  • UnitOfWorkUnitOfWork.Discard(), the third answer to a refused row
  • function — why a fault discards the writes: a function is one transaction
  • constraints · invariant — the rules that raise ValidationException for you
  • AssertAssert.Throws<T>, which is how you prove a function refuses what it should

Related

try / catch / finally

Handles a fault. C#'s syntax, including typed catches, catch filters and finally. The rows written inside a try block…

function

A function is a top-level unit of work, written like a C# method — a return type, a name, typed parameters, a body. It…

UnitOfWork

Every body accumulates its writes in a unit of work rather than sending them one at a time. UnitOfWork.Commit() makes…

constraints

The per-member rules the database enforces — Required, Unique, MaxLength/MinLength, Min/Max, Pattern, and the…

invariant

A row-level rule spanning several members, checked when the row is written. Use it when a constraint on one member is…

Assert

The assertions a test makes. Beyond the usual equality and null checks there are comparisons (Greater, Less, InRange)…