Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
Changelog for package behaviortree_cpp
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Forthcoming
-----------
* Add Finally control node: always runs a cleanup child after the main child finishes, fails, throws, or is halted

4.9.0 (2026-02-11)
------------------
* Fix Blackboard thread-safety: 6 data races fixed, use shared_mutex for storage
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ list(APPEND BT_SOURCE
src/controls/sequence_node.cpp
src/controls/sequence_with_memory_node.cpp
src/controls/switch_node.cpp
src/controls/finally_node.cpp
src/controls/try_catch_node.cpp
src/controls/while_do_else_node.cpp

Expand Down
1 change: 1 addition & 0 deletions include/behaviortree_cpp/behavior_tree.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "behaviortree_cpp/actions/updated_action.h"
#include "behaviortree_cpp/condition_node.h"
#include "behaviortree_cpp/controls/fallback_node.h"
#include "behaviortree_cpp/controls/finally_node.h"
#include "behaviortree_cpp/controls/if_then_else_node.h"
#include "behaviortree_cpp/controls/parallel_all_node.h"
#include "behaviortree_cpp/controls/parallel_node.h"
Expand Down
48 changes: 48 additions & 0 deletions include/behaviortree_cpp/controls/finally_node.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#pragma once

#include "behaviortree_cpp/control_node.h"

namespace BT
{
/**
* @brief The Finally node ticks its first child ("main") and then always ticks
* its second child ("cleanup"), like try/finally.
*
* - Cleanup runs after main returns SUCCESS, FAILURE or SKIPPED.
* - If main throws, the exception is printed to stderr, main is halted,
* cleanup runs, and this node returns FAILURE.
* - If this node is halted while main is RUNNING, main is halted and cleanup
* is ticked once synchronously. If cleanup returns RUNNING, it is halted.
* - The node returns main's status, or FAILURE if cleanup fails.
* - Exceptions thrown by cleanup propagate from tick(). During halt they are
* printed to stderr instead, because halt() also runs from ~Tree().
*
* Requires exactly 2 children.
*/
class FinallyNode : public ControlNode
{
public:
FinallyNode(const std::string& name, const NodeConfig& config);

~FinallyNode() override = default;

FinallyNode(const FinallyNode&) = delete;
FinallyNode& operator=(const FinallyNode&) = delete;
FinallyNode(FinallyNode&&) = delete;
FinallyNode& operator=(FinallyNode&&) = delete;

static PortsList providedPorts()
{
return {};
}

void halt() override;

private:
bool in_cleanup_ = false;
NodeStatus main_status_ = NodeStatus::IDLE;

BT::NodeStatus tick() override;
};

} // namespace BT
1 change: 1 addition & 0 deletions src/bt_factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ BehaviorTreeFactory::BehaviorTreeFactory() : _p(new PImpl)
registerNodeType<IfThenElseNode>("IfThenElse");
registerNodeType<WhileDoElseNode>("WhileDoElse");
registerNodeType<TryCatchNode>("TryCatch");
registerNodeType<FinallyNode>("Finally");

registerNodeType<InverterNode>("Inverter");

Expand Down
95 changes: 95 additions & 0 deletions src/controls/finally_node.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#include "behaviortree_cpp/controls/finally_node.h"

#include <iostream>

namespace BT
{
FinallyNode::FinallyNode(const std::string& name, const NodeConfig& config)
: ControlNode::ControlNode(name, config)
{
setRegistrationID("Finally");
}

void FinallyNode::halt()
{
if(!in_cleanup_ && status() == NodeStatus::RUNNING && children_nodes_.size() == 2)
{
haltChild(0);
// halt() also runs from ~Tree(), where a propagating exception terminates.
try
{
if(children_nodes_[1]->executeTick() == NodeStatus::RUNNING)
{
haltChild(1);
}
}
catch(const std::exception& ex)
{
std::cerr << "[" << name() << "]: Finally cleanup threw during halt: " << ex.what()
<< std::endl;
}
}
in_cleanup_ = false;
ControlNode::halt();
}

NodeStatus FinallyNode::tick()
{
if(children_nodes_.size() != 2)
{
throw LogicError("[", name(), "]: Finally requires exactly 2 children");
}

if(!isStatusActive(status()))
{
in_cleanup_ = false;
}

setStatus(NodeStatus::RUNNING);

if(!in_cleanup_)
{
try
{
main_status_ = children_nodes_[0]->executeTick();
}
catch(const std::exception& ex)
{
std::cerr << "[" << name() << "]: Finally caught an exception from its main child, "
<< "running cleanup and returning FAILURE: " << ex.what() << std::endl;
haltChild(0);
main_status_ = NodeStatus::FAILURE;
}

if(main_status_ == NodeStatus::RUNNING)
{
return NodeStatus::RUNNING;
}
if(main_status_ == NodeStatus::IDLE)
{
throw LogicError("[", name(), "]: A child should not return IDLE");
}
in_cleanup_ = true;
}

const NodeStatus cleanup_status = children_nodes_[1]->executeTick();
if(cleanup_status == NodeStatus::RUNNING)
{
return NodeStatus::RUNNING;
}

resetChildren();
in_cleanup_ = false;
if(cleanup_status == NodeStatus::FAILURE)
{
return NodeStatus::FAILURE;
}
if(main_status_ == NodeStatus::SKIPPED)
{
// executeTick() keeps our RUNNING status on SKIPPED, and halt() would rerun cleanup.
resetStatus();
}
return main_status_;
}

} // namespace BT
5 changes: 5 additions & 0 deletions src/xml_parsing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,11 @@ void VerifyXML(const std::string& xml_text,
ThrowError(line_number, std::string("The node 'TryCatch' must have "
"at least 2 children"));
}
if(registered_name == "Finally" && children_count != 2)
{
ThrowError(line_number, std::string("The node 'Finally' must have "
"exactly 2 children"));
}
if(registered_name == "ReactiveSequence")
{
size_t async_count = 0;
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ set(BT_TESTS
gtest_switch.cpp
gtest_tree.cpp
gtest_try_catch.cpp
gtest_finally.cpp
gtest_exception_tracking.cpp
gtest_updates.cpp
gtest_wakeup.cpp
Expand Down
Loading
Loading