Transactional Try Blocks in ValueScript

voltrevo

Andrew Morris

Posted on March 30, 2023

Transactional Try Blocks in ValueScript

Exceptions are difficult to get right. A particularly tricky problem that’s easy to overlook is the possibility of partial evaluation inside try blocks.

Consider the following JavaScript program:

let a = new SmallQueue(["item1"]);
let b = new SmallQueue(["item2", "item3", "item4"]);

try {
  const item = a.pop();
  b.push(item);
} catch (e) {
  console.error("Failed to move item from a to b", e);
}

console.log({ a, b });
Enter fullscreen mode Exit fullscreen mode

If 4 items is not too many for SmallQueue, then we can expect an output like this:

{
  a: [],
  b: ["item2", "item3", "item4", "item1"],
}
Enter fullscreen mode Exit fullscreen mode

Otherwise, we’ll log the error from b.push, but unfortunately "item1" will be lost forever:

{
  a: [],
  b: ["item2", "item3", "item4"],
}
Enter fullscreen mode Exit fullscreen mode

This is not good. To fix this, we could check whether b is full first:

let a = new SmallQueue(["item1"]);
let b = new SmallQueue(["item2", "item3", "item4"]);

try {
  if (b.isFull()) {
    throw new Error("b is full");
  }

  const item = a.pop();
  b.push(item);
} catch (e) {
  console.error("Failed to move item from a to b", e);
}

console.log({ a, b });
Enter fullscreen mode Exit fullscreen mode

This will fix the problem, but this type of solution scales poorly. It requires us to understand the exception that might cause partial evaluation and proactively protect against it. The point of exceptions is often to model unexpected things. If you’re not expecting it, then there’s an elevated chance you won’t get this protection code right.

Another option is to save a copy of the variables that might change and restore them on catch:

let a = new SmallQueue(["item1"]);
let b = new SmallQueue(["item2", "item3", "item4"]);

const aCopy = a.copy();
const bCopy = b.copy();

try {
  const item = a.pop();
  b.push(item);
} catch (e) {
  a = aCopy;
  b = bCopy;
  console.error("Failed to move item from a to b", e);
}

console.log({ a, b });
Enter fullscreen mode Exit fullscreen mode

This has potential to scale better, but it has problems of its own:

  1. You might not do it because it’s verbose and feels unnecessarily defensive
  2. It requires a copy operation to be defined
  3. If the copy operation exists, it might not be efficient
  4. If a.pop has side effects, those side effects won’t be reverted

Yet another strategy is to insist that any function call that might throw an exception is individually handled. Sometimes this strategy is enforced at the language level, particularly in languages that don’t have stack-unwinding exceptions, like Go and Rust.

let a = new SmallQueue(["item1"]);
let b = new SmallQueue(["item2", "item3", "item4"]);

let item;

try {
  item = a.pop();
} catch (e) {
  console.error("Failed to move item from a to b", e);
  console.log({ a, b });
  return;
}

try {
  b.push(item);
} catch (e) {
  a.unPop(item);
  console.error("Failed to move item from a to b", e);
}

console.log({ a, b });
Enter fullscreen mode Exit fullscreen mode

Problems with this approach:

  1. Extremely verbose
  2. a.unPop(item) requires us to understand that this operation is needed when b.push(item) fails, which is still easy to miss

Surely there is a better way.


In ValueScript, try blocks are transactional — they either run to completion or they’re reverted.

Our original program does what we wanted without any modifications:

let a = new SmallQueue(["item1"]);
let b = new SmallQueue(["item2", "item3", "item4"]);

try {
  const item = a.pop();
  b.push(item);
} catch (e) {
  console.error("Failed to move item from a to b", e);
}

console.log({ a, b });
Enter fullscreen mode Exit fullscreen mode
{
  a: ["item1"],
  b: ["item2", "item3", "item4"],
}
Enter fullscreen mode Exit fullscreen mode

ValueScript uses the copy-and-restore method, but the problems mentioned earlier do not apply:

  • 1. “You might not do it”
    • The ValueScript compiler inserts the instructions automatically
  • 2. “Requires copy operation”
    • ValueScript uses value semantics, it just uses aCopy = a, and this is equivalent to copying
  • 3. “copy might not be efficient”
    • ValueScript uses copy-on-write all the way down, it’s quite efficient
  • 4. “Side effects of a.pop won’t be reverted”
    • ValueScript doesn’t have side effects

I should note that (4) will be weakened in the future, out of necessity. ValueScript doesn’t yet have foreign functions (eg fetch, console.log), but when it does, it won’t be able to revert the side effects of these functions*. You’ll still have to worry about that, but at least ValueScript covers all the internal effects of partial evaluation, so you’ll have more bandwidth to focus on handling the external effects of partial evaluation.

*some awesome foreign functions might include revert operations and these would be automatically called


ValueScript is a dialect of TypeScript with value semantics. It has an online playground including a demonstration of this article’s program, and it’s open source.

💖 💪 🙅 🚩
voltrevo
Andrew Morris

Posted on March 30, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related