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 });
If 4 items is not too many for SmallQueue
, then we can expect an output like this:
{
a: [],
b: ["item2", "item3", "item4", "item1"],
}
Otherwise, we’ll log the error from b.push
, but unfortunately "item1"
will be lost forever:
{
a: [],
b: ["item2", "item3", "item4"],
}
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 });
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 });
This has potential to scale better, but it has problems of its own:
- You might not do it because it’s verbose and feels unnecessarily defensive
- It requires a copy operation to be defined
- If the copy operation exists, it might not be efficient
- 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 });
Problems with this approach:
- Extremely verbose
-
a.unPop(item)
requires us to understand that this operation is needed whenb.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 });
{
a: ["item1"],
b: ["item2", "item3", "item4"],
}
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
- ValueScript uses value semantics, it just uses
- 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.
Top comments (0)