To assert() or not to assert()?

Background

I’ve noticed that I’m using and recommending to use assert() in our TypeScript projects. Particularly, to be used in runtime, including input validation sometimes, as opposed to the common use case which is in tests. While one colleague was fully onboard, another one noted that use of assert() in runtime is non-idiomatic and is therefore confusing and we should use differently-named function(s). While partially agreed, I didn’t think these statements were so clear cut and started digging.

My Opinion

… on use of assertions by use case, after some research (see research results below).

Tests

That’s the natural habitat of assertions and where they should be used freely. I don’t think this idea is a surprise to anybody.

Runtime – Programming Errors

In context of languages that keep assertions in production (ex: JavaScript/TypeScript)…

Yes, I do recommend using assertions for catching programming errors at runtime. Assertions here express the intent clearly and concisely. They are explicit assumptions. Failed assertion means a programming error caught at runtime. In context of an HTTP(S) server, these failed assertions should return HTTP status 500 Internal Server Error.

This includes deployment/configuration errors. For example, if there is an environment variable that is required for your code to run – you should assert() that it was passed. Side note: do it as soon as possible and fail the initialization if it’s missing.

I tend to include invalid data fetched from database in this category too.

Runtime – Input Validation

Assertions are not a good match for input validation. A more elegant and appropriate solution should be used. In TypeScript, for example, it could involve zod or some other validation libraries. For HTTP(S) server side code, input validation failure should return HTTP status 400 Bad Request.


If you are short on time, you can stop here. Below goes some interesting information about assertions though.


Research Results

I have looked into sources of Node.js and undici (deps/undici in Node.js), and vscode, and others for some “authoritative” answers. I have found:

  • Extensive use of assert() at runtime for asserting invariants. This runtime usage is still aligned with the semantics of assert() in tests: failed assertion means programming error.
  • Specifically, these projects, do not use assert() for arguments validation (with the exception of one borderline case).
  • Node.js
    • Uses a bunch of validateFoo functions for argument validation: validateNumber, validateString, validateOneOf, etc that throw appropriate errors. What makes them special and different from assert() is that they produce messages meaningful for argument validation. For that, validateFoo functions have name parameter. And of course, validateFoo functions they convey different intent.
    • States “AssertionErrors are a special class of error that can be triggered when Node.js detects an exceptional logic violation that should never occur. These are raised typically by the node:assert module.”
  • vscode defines and uses functions such as assert(), assertFn() and assertType().
    • Interestingly enough, assertMarkdownFiles() in extensions/github/src/pushErrorHandler.ts doesn’t actually assert anything but filters (I opened GitHub issue).
  • NestJS
    • Defines and uses assertFoo methods NestApplicationContext#assertNotInPreviewMode() and RouterResponseController#assertObservable()at runtime.
    • Uses Channel#assertExchange()method from amqplib. Here, “assert” is used in a sense of “make sure something exists”, which is named “ensure” in some other contexts. TODO: google “ensure”. use of “ensure” in programming
  • assert-plus, a transitive dependency of NestJS, is a wrapper around assert() but specifically for arguments validation. Given around 10M weekly downloads as of writing, it can’t be dismissed.

It looks like some are using “assert” in the broader sense of “this situation was not supposed to happen”, including argument validation while others use “assert” in the stricter sense of catching programming errors.

What is assert()?

I’m intentionally mixing different sources here to provide broader view.

Node.js documentation says:

The node:assert module provides a set of assertion functions for verifying invariants.

C# documentation (for Microsoft.VisualStudio.TestTools.UnitTesting Namespace, Assert class) says:

A collection of helper classes to test various conditions within unit tests. If the condition being tested is not met, an exception is thrown.

C# documentation (for Debug.Assert Method) says:

Checks for a condition; if the condition is false, outputs messages and displays a message box that shows the call stack.

By default, the Debug.Assert method works only in debug builds. Use the Trace.Assert method if you want to do assertions in release builds. For more information, see Assertions in Managed Code.

C++ online reference says:

The definition of the macro assert depends on another macro, NDEBUG, which is not defined by the standard library.

If NDEBUG is defined as a macro name at the point in the source code where <cassert> or <assert.h> is included, the assertion is disabled: assert does nothing.

Java spec says:

An assertion is an assert statement containing a boolean expression. An assertion is either enabled or disabled. If an assertion is enabled, execution of the assertion causes evaluation of the boolean expression and an error is reported if the expression evaluates to false. If the assertion is disabled, execution of the assertion has no effect whatsoever.

… assertions should not be used for argument checking in public methods. Argument checking is typically part of the contract of a method, and this contract must be upheld whether assertions are enabled or disabled.

JUnit documentation says:

Assertions is a collection of utility methods that support asserting conditions in tests.

Lisp documentation says:

assert assures that test-form evaluates to true. If test-form evaluates to false, assert signals a correctable error

… and goes on to provide (assert ...) example for arguments validation

C2 wiki says:

An assertion is a boolean expression at a specific point in a program which will be true unless there is a bug in the program.

How Assertions are Used?

The use cases from above range widely:

  • Tests only (C#, JUnit)
  • Runtime during development only (C#, C++, Java), no-op in production code
  • Runtime (C#, Lisp)

Yes, C# has all three variations as separate facilities.

Some examples from Node.js follow.

Tests

Node.js, node/test/abort/test-abort-backtrace.js

Note that this form gets both of the actual result and the expected result. It can therefore print both when there is discrepancy.

const child = cp.spawnSync(`${process.execPath}`, [`${__filename}`, 'child']);

const stderr = child.stderr.toString();
assert.strictEqual(child.stdout.toString(), '');

...
assert(jsStack.some((frame) => frame.includes(__filename)));

Catching Programming Error at Runtime

Node.js, node/lib/_http_agent.js, expressing intent using assert.fail() rather than just throwing an error.

function createConnection(...args) {

  ...

  // This should be unreachable because proxy config should be null for other protocols.

  assert.fail(`Unexpected proxy protocol ${proxyProtocol}`);

};

Asserting Invariant at Runtime

Node.js, node/lib/_http_client.js

function socketOnData(d) {

  const socket = this;

  const req = this._httpMessage;

  const parser = this.parser;


  assert(parser && parser.socket === socket);

  ...

}

“It’s complicated”

Node.js, node/lib/https.js

In this case, the phrasing suggests catching internal programming error but createConnection() – the caller – “custom agents may override this method to provide greater flexibility,” so assert() here can be seen as (borderline) argument validation.

function getTunnelConfigForProxiedHttps(agent, reqOptions) {

  ...
  
const requestHost = ipType === 6 ? `[${reqOptions.host}]` : reqOptions.host;
const requestPort = reqOptions.port || agent.defaultPort;
const endpoint = `${requestHost}:${requestPort}`;
  
// The ClientRequest constructor should already have validated the host and the port.
  
// When the request options come from a string invalid characters would be stripped away,
  
// when it's an object ERR_INVALID_CHAR would be thrown. Here we just assert in case

  // agent.createConnection() is called with invalid options.
  
assert(!endpoint.includes('\r'));

  assert(!endpoint.includes('\n'));
  ...
}

Interesting Findings Along the Way

Type Checking

Some of these assert()s in JavaScript code check for correct types at runtime and wouldn’t be needed in TypeScript which would check these statically, ahead of time.

Custom Exceptions

In Node.js, the message parameter can be an instance of Error and in this case, it’s thrown instead of the usual AssertionError with the string message message. It was introduced in 2017 to “support a way to provide a custom Error type for assertions. This will help make assert more useful for validating types and ranges.”

??= for caching

Node.js, lib/internal/assert.js

Elegant use of ??= for caching.

let error;

function lazyError() {

  return error ??= require('internal/errors').codes.ERR_INTERNAL_ASSERTION;

}

ThrowIfNull

C# has ArgumentNullException.ThrowIfNull, which I find interesting language design choice – static method on the exception that throws conditionally.

 


Hope it was interesting and enjoyable. What’s your opinion on the topic? Leave your comment below. Have a nice day!