{"record":{"id":"ed522145056f0590","repo":"mybatis/mybatis-3","slug":"error-cannot-close-no-managed-session-is-start","errorCode":null,"errorMessage":"Error:  Cannot close.  No managed session is started.","messagePattern":"Error:  Cannot close\\.  No managed session is started\\.","errorType":"exception","errorClass":"SqlSessionException","httpStatus":null,"severity":"error","filePath":"src/main/java/org/apache/ibatis/session/SqlSessionManager.java","lineNumber":329,"sourceCode":"      throw new SqlSessionException(\"Error:  Cannot rollback.  No managed session is started.\");\n    }\n    sqlSession.rollback(force);\n  }\n\n  @Override\n  public List<BatchResult> flushStatements() {\n    final SqlSession sqlSession = localSqlSession.get();\n    if (sqlSession == null) {\n      throw new SqlSessionException(\"Error:  Cannot rollback.  No managed session is started.\");\n    }\n    return sqlSession.flushStatements();\n  }\n\n  @Override\n  public void close() {\n    final SqlSession sqlSession = localSqlSession.get();\n    if (sqlSession == null) {\n      throw new SqlSessionException(\"Error:  Cannot close.  No managed session is started.\");\n    }\n    try {\n      sqlSession.close();\n    } finally {\n      localSqlSession.remove();\n    }\n  }\n\n  private class SqlSessionInterceptor implements InvocationHandler {\n    public SqlSessionInterceptor() {\n      // Prevent Synthetic Access\n    }\n\n    @Override\n    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n      final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();\n      if (sqlSession != null) {\n        try {","sourceCodeStart":311,"sourceCodeEnd":347,"githubUrl":"https://github.com/mybatis/mybatis-3/blob/008069adb1b089579b5dcba87ee591908b263274/src/main/java/org/apache/ibatis/session/SqlSessionManager.java#L311-L347","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Guard close with isManagedSessionStarted() and close exactly once per started session.","Centralize session lifecycle in one owner (filter/interceptor) and remove ad-hoc close calls elsewhere.","Call close() only after a successful startManagedSession() on the same thread."],"exampleFix":"// before\n} finally { sqlSessionManager.close(); }  // throws on second pass / when never started\n// after\n} finally {\n  if (sqlSessionManager.isManagedSessionStarted()) {\n    sqlSessionManager.close();\n  }\n}","handlingStrategy":"validation","validationCode":"if (sqlSessionManager.isManagedSessionStarted()) {\n  sqlSessionManager.close();\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["close() is not idempotent on SqlSessionManager — call it exactly once per started session.","Give the session lifecycle a single owner (filter/interceptor) and delete ad-hoc close calls.","Guard every close with isManagedSessionStarted()."],"tags":["mybatis","session-management","double-close","threadlocal"],"backgroundTag":null,"analyzedSha":"008069adb1b089579b5dcba87ee591908b263274","analyzedAt":"2026-08-14T13:07:10.264Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}