Error.assert

Feedback appreciated :pray:

Synopsis

Provide a standardized way to assert that an expression is true.

Motivation

Developers often want to ensure a condition is met before allowing their program to proceed. There is currently no standardized way to do this independent from the host environment other than manually checking if an expression is true and conditionally throwing, as demonstrated by this example:

if (user == null) {
  throw new Error('User is not defined');
}

Prior Art

  • Node's assert library, specifically assert.ok
  • Rust, Ruby, Python, Elixir, C++, Java, and many other languages have a built in assert function

Solution

The proposed solution is to add a class method to the intrinsic object Error named assert. This would allow the above example to be refactored into:

Error.assert(user != null, 'User is not defined');

A shim for this would look like:

Error.assert = function(assertion, ...rest) {
  if (!assertion) {
    throw new this(...rest)
  }
}

The other core error constructors that inherit from Error could then also be used in a similar fashion:

TypeError.assert(user != null, 'User is not defined');
5 Likes

Props to @NilSet who also helped ideate this.

most environments have console.assert, which does basically what you're asking. it's not part of js itself but it is everything that follows the console specification.

1 Like

The main issue with console.assert is that it does not throw and only prints to the console. In the below example 'bar' is not logged.

try {
  console.assert(false, 'foo');
} catch(e) {
  console.log('bar');
}
// Assertion failed: foo
2 Likes

Why has nobody started a Stage-0 proposal for this?

For what exactly? Early stage proposals need a problem statement (not necessarily a potential solution). What problem do you hope to solve?

Problem statement: Improve ergonomics of enforcing constraints in your code.

Additional context around the problem statement:

When writing code, sometimes you want to document that a certain constraint will always hold at a certain spot in your code, for example, "the array should be empty at this spot" or "this property should not be undefined anymore at this point".

Alternative options we can use today:

  • Leaving a comment. While this does technically document the constraint, that documentation isn't enforced, and it can make it difficult to believe the comment.
  • Using console.assert(). This is appropriate for some situations where if the constraint is accidentally broken, you're fine with logging a warning and continuing on. But in many scenarios, the constraint exists because the code that follows depends on that constraint being upheld, and it would be better to treat failure to uphold the constraint as an error, not a warning.
  • Checking for the constraint in an "if" and throwing an error. This works, but it's a rather verbose alternative. We want the constraint to be concise, because it's like a footnote to the logic in the function - "hey, at this point, you can expect this to be true", and having unnecessarily large footnotes can make the overall function harder to read.
1 Like

I would've written "invariant", but "constraint" is also correct.

Thank you for elaborating!

There's another use-case:
assert(false) can be used to mark code that should be unreachable. It'd be more appropriate to have an assertUnreachable as it communicates intent better (both for humans and linters)

FWIW, we've found that for assertions to be useful they need detailed messages, which usually means including the asserted value of components forming the assertion. Building that message has a cost that shouldn't be incurred when the check doesn't fail (the normal case).

What that forced us to do is write our assertions as conditions so the message would be constructed only in the exceptional case.

We would prefer to write this as:

assertedExpression || Fail`error message with ${details}`;

But TS inference fails because of both the logical expression, and the tagged literal. (The never return type of Fail is ignored)

2 Likes

Ah, yeah, that's the word :)

That's a good point. So if we do introduce an assert function, perhaps it would make sense to make the message argument optionally be a lambda instead of a string, where the lambda only gets called if the assertion fails.

Good point about the lazy message creation.

Were there other reasons you were using a tagged template instead of Fail(string)? I assume it also customised the toString handling, perhaps avoiding cases where it could throw:

let o = Object.create(null);
let s = `${o}`; // throws
1 Like

Yeah we use tagged templates because we also don't simply process the arguments: we by default put only the type of the arguments in the public message property and annotate our errors with the full details of the arguments that only enhanced diagnostics sinks have access to (same for stack traces, it's not available to regular code holding the error object)

1 Like

Here are the TS issues for reference:

1 Like

For a general-purpose assert(boolean) that's a real problem, because the dev must specify the message manually. However, if we specialize assertions:

  • assertEq(0, 1): "Expected 0 but found 1"
  • assertGt(0, 1): "0 is not greater than 1"
  • assertType("0", "array"): "Expected "0" to be an Array, but it's a String"
  • etc...

Then the specialized versions can have default template-messages that are more informative than "Assertion failed"

Sure but those are special cases. At the end of the day I personally find assertions written as logical expressions fairly readable, and as mentioned they would be the most performant in all cases, so I'm not convinced JS needs assertion APIs. The main problem is not the language but the type tooling ecosystem which is discouraging that pattern.

@mhofman - would you consider adding a native Fail template tag to the language? So it's easier to use a pattern like that without having to write a fail function in every project that needs it?

It would be nice to have something available that helps solve the "concisely enforcing invariants" use case. I know it's not a lot of code to write your own assert/fail/whatever function, but given that I'm adding one to almost every project I write, and even for tiny code snippets I'm sharing online, it sure would be nice to have something more native here.