New Issue: [Feature Request]: Adjust the 'contextManager' API to better handle 'throws'

28148, "dlongnecke-cray", "[Feature Request]: Adjust the 'contextManager' API to better handle 'throws'", "2025-12-06T01:39:53Z"

The problem introduced by #28108

The problem is that users can write throwing code in any manage statement, even if that manage statement is in a non-throwing context.

Today, manage statements are implemented with some workarounds (as of #28108) in order to support throwing statements in their body as well as exitContext() methods that can throw.

The body of a manage statement can throw, however exitContext() methods can be marked with throws and can throw for any reason. Because of this, the compiler will potentially see the body of almost every manage statement as a throwing point.

// A simple context manager.
record r {
  var throwError = false;
  proc ref enterContext() ref do return this;
  proc exitContext(in e: owned Error?) throws {
    if e then throw e;
    if throwError then throw new Error('From 

Notice that neither `case1` or `case2` are throwing procedures. However, the `manage` statement in either example contains throwing code, despite not being encapsulated in a `try`/`catch`!

Instead of requiring users to wrap all their `manage` statements in a `try`/`catch`, #28108 opted to use a workaround: every `manage` statement that appears in a _non-throwing_ context (such as both `case1` and `case2`) is wrapped in an implicit `try!` block.

The reason why I chose this behavior is because I considered needing to encapsulate almost _every_ `manage` statement in a `try`/`catch` an unacceptable burden (way too much boilerplate!).

**This has the weird side effect of allowing users to write throwing code in any `manage` statement.**

This is a bit of an oddity, because normally when a user writes throwing code in a non-throwing context, they either have to annotate their method with `throws`, wrap the code in a `try`/`catch`, or be in a non-strict error handling mode (where the compiler implicitly will annotate throwing code with `try!`).

## We should fix this oddity...

Ideally, we should move towards the normally prescribed behavior: if a `manage` containing throwing code is written in a non-throwing context, then the compiler should request that the user wrap their throwing code in a `try` / `catch` block (or annotate their procedure with `throws`, etc).

As of today, `manage` statements can be seen as throwing points due to two reasons:
- If the user wrote a throwing expression
- If the `exitContext()` method on any manager used in the statement throws

The first is good, it's what we want to happen - if the user wrote throwing code in a non-throwing context, then they have to handle thrown errors in some fashion.

The second is the problem and is I think ultimately a deficiency of the `contextManager` API: the `exitContext()` method is overloaded because it must be marked with `throws` in order to handle the possibility of a passed in `Error` thrown from the `manage` statement body. This `throws` tag has the unfortunate side-effect of polluting `manage` statements written in non-throwing contexts.

So to fix this issue, I think we need to adjust `exitContext()`.

## The `contextManager` API
 
Today, the `contextManager` interface is defined (internally) like the following:

```chapel
  interface contextManager {
      type contextReturnType;

      pragma "ifc any return intent"
      proc Self.enterContext() ref : contextReturnType;
      proc Self.exitContext(in error: owned Error?);
  }

That is, it is an interface that requires 2 methods:

  • The first is named enterContext() and can have any return intent and any return type
  • The second is named exitContext() and is passed an in owned Error?

Today, the exitContext() interface method does not have the throws tag, but to properly represent the manage statement, throws ought to be a requirement.

Option A: Splitting exitContext() into two methods

My changes are focused on exitContext(). Today, the problem is that this method is pulling double duty - it both reacts to thrown errors (via the Error formal) as well as throws them (either what is passed in, or an arbitrary error, for any reason).

I think that we should split this overloaded method in two - that is, we now have:

      proc Self.exitThrowingContext(in error: owned Error?): void throws;
      proc Self.exitContext(): void {
        try! exitThrowingContext(nil);
      }

We now have two methods.

The first, exitThrowingContext(), is called when either:

  • The compiler detects throwing code contained within the body of the manage statement
  • The compiler detects that the manage statement is in a throwing context

It is the only variant of exitContext() that can throw. If we have not already, we should make it a requirement of the interface that exitThrowingContext() must have the throws tag (and the interface rules should make the compiler complain when a implementation does not have that tag).

The second, exitContext(), cannot throw, and is not passed anything. It is called when both:

  • The compiler does not detect any throwing code in the manage statement body
  • And the manage statement is in a non-throwing context

The default implementation of exitContext() simply calls exitThrowingContext in a try!. Users can replace it with a different implementation if they wish.

This design tries to make exitThrowingContext() be called as often as possible in order to maximize the opportunity for users to throw errors.

In the event that a user writes throwing code in a manage statement body but that manage statement is in a non-throwing context, then exitThrowingContext() will still be selected even though the user has written an erroneous statement. That is OK - the user has handle the errors already:

  • If the user wraps the manage in a try/catch, then the manage is now safely in a throwing context and exitThrowingContext() can be called
  • If the user wraps the body in a try/catch or try!, then the body of the manage is no longer a throwing point, and exitContext() will be called instead

Option B: Change the signature of exitContext()

The second idea is to adjust the signature of exitContext() so that it no longer contains the throws tag.

proc Self.exitContext(in e: owned Error?): void throws;

Becomes...

proc Self.exitContext(in thrownError: owned Error?, out errorToPropagate: owned Error?=thrownError): void;

The problem that is causing so much grief with exitContext() today is that it effectively "always" has the throws tag, which means that it pollutes non-throwing contexts (even if the body of exitContext() never even throws!).

So my thought here is that we could just adjust the method so that it never has the throws tag.

If there is a passed in thrown error, the user is still able to replace it by setting a new out formal which functions similarly to the old throws tag.

The compiler will additionally adjust how it lowers the manage statement:

  • If the manage statement contains throwing code or it is written in a throwing context, then the compiler will make sure to throwthe final error computed byexitContext()` upwards into the containing scope
  • Otherwise, it will not bother emitting a throw, because it is not legally possibleexitContext()

Notice that neither case1 or case2 are throwing procedures. However, the manage statement in either example contains throwing code, despite not being encapsulated in a try/catch!

Instead of requiring users to wrap all their manage statements in a try/catch, #28108 opted to use a workaround: every manage statement that appears in a non-throwing context (such as both case1 and case2) is wrapped in an implicit try! block.

The reason why I chose this behavior is because I considered needing to encapsulate almost every manage statement in a try/catch an unacceptable burden (way too much boilerplate!).

This has the weird side effect of allowing users to write throwing code in any manage statement.

This is a bit of an oddity, because normally when a user writes throwing code in a non-throwing context, they either have to annotate their method with throws, wrap the code in a try/catch, or be in a non-strict error handling mode (where the compiler implicitly will annotate throwing code with try!).

We should fix this oddity...

Ideally, we should move towards the normally prescribed behavior: if a manage containing throwing code is written in a non-throwing context, then the compiler should request that the user wrap their throwing code in a try / catch block (or annotate their procedure with throws, etc).

As of today, manage statements can be seen as throwing points due to two reasons:

  • If the user wrote a throwing expression
  • If the exitContext() method on any manager used in the statement throws

The first is good, it's what we want to happen - if the user wrote throwing code in a non-throwing context, then they have to handle thrown errors in some fashion.

The second is the problem and is I think ultimately a deficiency of the contextManager API: the exitContext() method is overloaded because it must be marked with throws in order to handle the possibility of a passed in Error thrown from the manage statement body. This throws tag has the unfortunate side-effect of polluting manage statements written in non-throwing contexts.

So to fix this issue, I think we need to adjust exitContext().

The contextManager API

Today, the contextManager interface is defined (internally) like the following:

  interface contextManager {
      type contextReturnType;

      pragma "ifc any return intent"
      proc Self.enterContext() ref : contextReturnType;
      proc Self.exitContext(in error: owned Error?);
  }

That is, it is an interface that requires 2 methods:

  • The first is named enterContext() and can have any return intent and any return type
  • The second is named exitContext() and is passed an in owned Error?

Today, the exitContext() interface method does not have the throws tag, but to properly represent the manage statement, throws ought to be a requirement.

Option A: Splitting exitContext() into two methods

My changes are focused on exitContext(). Today, the problem is that this method is pulling double duty - it both reacts to thrown errors (via the Error formal) as well as throws them (either what is passed in, or an arbitrary error, for any reason).

I think that we should split this overloaded method in two - that is, we now have:

      proc Self.exitThrowingContext(in error: owned Error?): void throws;
      proc Self.exitContext(): void {
        try! exitThrowingContext(nil);
      }

We now have two methods.

The first, exitThrowingContext(), is called when either:

  • The compiler detects throwing code contained within the body of the manage statement
  • The compiler detects that the manage statement is in a throwing context

It is the only variant of exitContext() that can throw. If we have not already, we should make it a requirement of the interface that exitThrowingContext() must have the throws tag (and the interface rules should make the compiler complain when a implementation does not have that tag).

The second, exitContext(), cannot throw, and is not passed anything. It is called when both:

  • The compiler does not detect any throwing code in the manage statement body
  • And the manage statement is in a non-throwing context

The default implementation of exitContext() simply calls exitThrowingContext in a try!. Users can replace it with a different implementation if they wish.

This design tries to make exitThrowingContext() be called as often as possible in order to maximize the opportunity for users to throw errors.

In the event that a user writes throwing code in a manage statement body but that manage statement is in a non-throwing context, then exitThrowingContext() will still be selected even though the user has written an erroneous statement. That is OK - the user has handle the errors already:

  • If the user wraps the manage in a try/catch, then the manage is now safely in a throwing context and exitThrowingContext() can be called
  • If the user wraps the body in a try/catch or try!, then the body of the manage is no longer a throwing point, and exitContext() will be called instead

Option B: Change the signature of exitContext()

The second idea is to adjust the signature of exitContext() so that it no longer contains the throws tag.

proc Self.exitContext(in e: owned Error?): void throws;

Becomes...

proc Self.exitContext(in thrownError: owned Error?, out errorToPropagate: owned Error?=thrownError): void;

The problem that is causing so much grief with exitContext() today is that it effectively "always" has the throws tag, which means that it pollutes non-throwing contexts (even if the body of exitContext() never even throws!).

So my thought here is that we could just adjust the method so that it never has the throws tag.

If there is a passed in thrown error, the user is still able to replace it by setting a new out formal which functions similarly to the old throws tag.

The compiler will additionally adjust how it lowers the manage statement:

  • If the manage statement contains throwing code or it is written in a throwing context, then the compiler will make sure to throwthe final error computed byexitContext()` upwards into the containing scope
  • Otherwise, it will not bother emitting a throw, because it is not legally possible!');
    }
    }

proc case1() {
// The manage body can throw...
manage new r() do throw Error('From manage statement body!');
}

proc case2() {
// But the 'exitContext()' can also throw for any reason!
manage new r(throwError=true) do writeln('No throw in manage body!');
}

case1();
case2();


Notice that neither `case1` or `case2` are throwing procedures. However, the `manage` statement in either example contains throwing code, despite not being encapsulated in a `try`/`catch`!

Instead of requiring users to wrap all their `manage` statements in a `try`/`catch`, #28108 opted to use a workaround: every `manage` statement that appears in a _non-throwing_ context (such as both `case1` and `case2`) is wrapped in an implicit `try!` block.

The reason why I chose this behavior is because I considered needing to encapsulate almost _every_ `manage` statement in a `try`/`catch` an unacceptable burden (way too much boilerplate!).

**This has the weird side effect of allowing users to write throwing code in any `manage` statement.**

This is a bit of an oddity, because normally when a user writes throwing code in a non-throwing context, they either have to annotate their method with `throws`, wrap the code in a `try`/`catch`, or be in a non-strict error handling mode (where the compiler implicitly will annotate throwing code with `try!`).

## We should fix this oddity...

Ideally, we should move towards the normally prescribed behavior: if a `manage` containing throwing code is written in a non-throwing context, then the compiler should request that the user wrap their throwing code in a `try` / `catch` block (or annotate their procedure with `throws`, etc).

As of today, `manage` statements can be seen as throwing points due to two reasons:
- If the user wrote a throwing expression
- If the `exitContext()` method on any manager used in the statement throws

The first is good, it's what we want to happen - if the user wrote throwing code in a non-throwing context, then they have to handle thrown errors in some fashion.

The second is the problem and is I think ultimately a deficiency of the `contextManager` API: the `exitContext()` method is overloaded because it must be marked with `throws` in order to handle the possibility of a passed in `Error` thrown from the `manage` statement body. This `throws` tag has the unfortunate side-effect of polluting `manage` statements written in non-throwing contexts.

So to fix this issue, I think we need to adjust `exitContext()`.

## The `contextManager` API
 
Today, the `contextManager` interface is defined (internally) like the following:

```chapel
  interface contextManager {
      type contextReturnType;

      pragma "ifc any return intent"
      proc Self.enterContext() ref : contextReturnType;
      proc Self.exitContext(in error: owned Error?);
  }

That is, it is an interface that requires 2 methods:

  • The first is named enterContext() and can have any return intent and any return type
  • The second is named exitContext() and is passed an in owned Error?

Today, the exitContext() interface method does not have the throws tag, but to properly represent the manage statement, throws ought to be a requirement.

Option A: Splitting exitContext() into two methods

My changes are focused on exitContext(). Today, the problem is that this method is pulling double duty - it both reacts to thrown errors (via the Error formal) as well as throws them (either what is passed in, or an arbitrary error, for any reason).

I think that we should split this overloaded method in two - that is, we now have:

      proc Self.exitThrowingContext(in error: owned Error?): void throws;
      proc Self.exitContext(): void {
        try! exitThrowingContext(nil);
      }

We now have two methods.

The first, exitThrowingContext(), is called when either:

  • The compiler detects throwing code contained within the body of the manage statement
  • The compiler detects that the manage statement is in a throwing context

It is the only variant of exitContext() that can throw. If we have not already, we should make it a requirement of the interface that exitThrowingContext() must have the throws tag (and the interface rules should make the compiler complain when a implementation does not have that tag).

The second, exitContext(), cannot throw, and is not passed anything. It is called when both:

  • The compiler does not detect any throwing code in the manage statement body
  • And the manage statement is in a non-throwing context

The default implementation of exitContext() simply calls exitThrowingContext in a try!. Users can replace it with a different implementation if they wish.

This design tries to make exitThrowingContext() be called as often as possible in order to maximize the opportunity for users to throw errors.

In the event that a user writes throwing code in a manage statement body but that manage statement is in a non-throwing context, then exitThrowingContext() will still be selected even though the user has written an erroneous statement. That is OK - the user has handle the errors already:

  • If the user wraps the manage in a try/catch, then the manage is now safely in a throwing context and exitThrowingContext() can be called
  • If the user wraps the body in a try/catch or try!, then the body of the manage is no longer a throwing point, and exitContext() will be called instead

Option B: Change the signature of exitContext()

The second idea is to adjust the signature of exitContext() so that it no longer contains the throws tag.

proc Self.exitContext(in e: owned Error?): void throws;

Becomes...

proc Self.exitContext(in thrownError: owned Error?, out errorToPropagate: owned Error?=thrownError): void;

The problem that is causing so much grief with exitContext() today is that it effectively "always" has the throws tag, which means that it pollutes non-throwing contexts (even if the body of exitContext() never even throws!).

So my thought here is that we could just adjust the method so that it never has the throws tag.

If there is a passed in thrown error, the user is still able to replace it by setting a new out formal which functions similarly to the old throws tag.

The compiler will additionally adjust how it lowers the manage statement:

  • If the manage statement contains throwing code or it is written in a throwing context, then the compiler will make sure to throwthe final error computed byexitContext()` upwards into the containing scope
  • Otherwise, it will not bother emitting a throw, because it is not legally possible