mybatis/mybatis-3 · error · SqlSessionException

Error: Cannot close. No managed session is started.

Error message

Error:  Cannot close.  No managed session is started.

What it means

SqlSessionManager.close() throws SqlSessionException 'Cannot close. No managed session is started.' when the thread-local is already empty. Unlike the operation methods, close() also removes the thread-local in a finally, so double-close is what usually triggers this: the first close() detaches the session, the second finds null and throws instead of being idempotent.

Solutions

  1. Guard close with isManagedSessionStarted() and close exactly once per started session.
  2. Centralize session lifecycle in one owner (filter/interceptor) and remove ad-hoc close calls elsewhere.
  3. Call close() only after a successful startManagedSession() on the same thread.

Example fix

// before
} finally { sqlSessionManager.close(); }  // throws on second pass / when never started
// after
} finally {
  if (sqlSessionManager.isManagedSessionStarted()) {
    sqlSessionManager.close();
  }
}
Defensive patterns

Strategy: validation

Validate before calling

if (sqlSessionManager.isManagedSessionStarted()) {
  sqlSessionManager.close();
}

Prevention

When it happens

Trigger: Calling close() twice (e.g. application shutdown hook plus request cleanup); close() without any prior startManagedSession(); close() in a finally block after an earlier close already ran.

Common situations: Generic framework cleanup layers that also close the session; refactoring code that moved a close() but left the old one; servlet-filter close racing with explicit close in a controller.

Related errors


AI-assisted analysis of mybatis/mybatis-3@008069adb1 (2026-08-14). Data as JSON: /api/errors/ed522145056f0590. Report an issue: GitHub.

Appendix: source

Thrown at src/main/java/org/apache/ibatis/session/SqlSessionManager.java:329

      throw new SqlSessionException("Error:  Cannot rollback.  No managed session is started.");
    }
    sqlSession.rollback(force);
  }

  @Override
  public List<BatchResult> flushStatements() {
    final SqlSession sqlSession = localSqlSession.get();
    if (sqlSession == null) {
      throw new SqlSessionException("Error:  Cannot rollback.  No managed session is started.");
    }
    return sqlSession.flushStatements();
  }

  @Override
  public void close() {
    final SqlSession sqlSession = localSqlSession.get();
    if (sqlSession == null) {
      throw new SqlSessionException("Error:  Cannot close.  No managed session is started.");
    }
    try {
      sqlSession.close();
    } finally {
      localSqlSession.remove();
    }
  }

  private class SqlSessionInterceptor implements InvocationHandler {
    public SqlSessionInterceptor() {
      // Prevent Synthetic Access
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();
      if (sqlSession != null) {
        try {

View on GitHub (pinned to 008069adb1)