How do I test that code throws an exception in EK9?

← Error Handling and Exceptions · Ref: Q306

EK9 provides assertThrows as a built-in keyword for verifying that an expression throws an expected exception type. It is only valid inside @Test programs.

BASIC SYNTAX

assertThrows(ExceptionType, expression)
The test passes if the expression throws an exception of the specified type. The test fails if no exception is thrown or a different type is thrown.

AS A STATEMENT

Use assertThrows when you only need to verify the exception occurs:

  assertThrows(Exception, riskyFunction())

If riskyFunction() does not throw, the test fails with a structured error message showing the location, expression, expected type, and what actually happened.

AS AN EXPRESSION (Capture)
Capture the thrown exception for further inspection:

  caught <- assertThrows(Exception, riskyFunction())
  assert caught.reason()?
  assert $caught == "Expected error message"

The returned value is the caught exception, allowing inspection of its reason, exit code, and custom fields.

FAILURE OUTPUT

When assertThrows fails, EK9 provides structured diagnostic output:

  assertThrows FAILED
    Location: ./dev/tests.ek9:5:3
    Expression: riskyFunction()
    Expected: org.ek9.lang::Exception
    Actual: No exception was thrown

This structured output is captured at compile time from the AST, not generated at runtime.

CUSTOM EXCEPTION TYPES

assertThrows works with custom exception types:

  assertThrows(ValidationError, validate(badInput))

The exception type must match exactly. A parent type will not catch a child type in assertThrows.

COMPILE-TIME RESTRICTION

assertThrows is only valid in @Test programs. Using it in production code causes a compile error. This prevents test assertions from accidentally appearing in production.

See Q134 for try/catch. See Q305 for require vs assert vs throw. See Q307 for assertDoesNotThrow. See Q209 for exception testing patterns.

Example

defines module qa.errorhandling.assertthrows

  defines class

    <?-
      Custom exception carrying a field name for validation errors.
    -?>
    ValidationError extends Exception
      field <- String()

      ValidationError()
        ->
          reason as String
          fieldName as String
        super(reason)
        this.field :=: fieldName

      field() as pure
        <- rtn as String: field

      default operator ?

  defines function

    <?-
      Always throws an exception for testing purposes.
    -?>
    failingOperation()
      throw Exception("Operation failed")

    <?-
      Validates and throws ValidationError for bad input.
    -?>
    validatePositive()
      -> number as Integer
      <- rtn as Integer: number

      if number < 0
        ex <- ValidationError("Must be positive", "number")
        throw ex

    <?-
      Safe operation that does not throw.
    -?>
    safeAdd() as pure
      ->
        a as Integer
        b as Integer
      <- rtn as Integer: a + b

  defines program

    @Test
    AssertThrowsStatementTest()
      stdout <- Stdout()

      stdout.println("=== assertThrows as statement ===")

      assertThrows(Exception, failingOperation())
      stdout.println("Verified: failingOperation() throws Exception")

      assertThrows(ValidationError, validatePositive(-5))
      stdout.println("Verified: validatePositive(-5) throws ValidationError")

    @Test
    AssertThrowsCaptureTest()
      stdout <- Stdout()

      stdout.println("=== assertThrows as expression (capture) ===")

      caught <- assertThrows(Exception, failingOperation())
      assert caught?
      stdout.println("Caught reason: " + caught.reason())

      valError <- assertThrows(ValidationError, validatePositive(-10))
      assert valError?
      stdout.println("Validation field: " + valError.field())
      stdout.println("Validation reason: " + valError.reason())

Common mistakes

E04030 — The first argument to assertThrows must be an Exception type or subclass. Using a non-exception type like String triggers E04030 — type must be of Exception type. Use Exception or a custom exception class that extends Exception. See ek9 -h E04030 for details.

Incorrect:

assertThrows(String, failingOperation())

Correct:

assertThrows(Exception, failingOperation())
Other ways to ask this
  • How does assertThrows work in EK9?
  • How do I verify an exception is thrown in a test?
  • What is the EK9 equivalent of JUnit assertThrows?

Coming from another language?

Java: JUnit assertThrows(() -> code, ExceptionType.class) returns the exception. Python: pytest.raises(ExceptionType) as context manager. Rust: #[should_panic] attribute on test function (coarse). Go: manual check with recover() in test helper. Kotlin: assertThrows<ExceptionType> { code }. C#: Assert.Throws<ExceptionType>(() => code). EK9: assertThrows(ExceptionType, expression) is a built-in keyword, not a library function. Compile-time restricted to @Test programs. Structured failure output with AST-captured source location.

Keywords: structured, verify, handle, exception, test, catch, testing, assertThrows, diagnostic, capture