The kubebuilder documentation on finalizers has the following example code for removing a finalizer from an object.

// remove our finalizer from the list and update it.
controllerutil.RemoveFinalizer(cronJob, myFinalizerName)
if err := r.Update(ctx, cronJob); err != nil {
    return ctrl.Result{}, err
}

I found when doing this, if the operation running above this in the Reconcile() function (typically to remove a remote resource from an API) this r.Update() call would return an error like this:

the object has been modified; please apply your changes to the latest version and try again

This can leave your object stuck in a Terminating state which can cause issues like Namespaces and other objects not be cleaned up correctly.

There are a few ways you could go about mitigating this issue, but using Patch() appears to be the fastest and most consistent approach.

// remove our finalizer from the list and update it.
f := ns.GetFinalizers()
for i, e := range f {
	if e == utils.BayFinalizerAwsAccount {
		p := []byte(fmt.Sprintf(`[{"op": "remove", "path": "/metadata/finalizers/%d"}]`, i))
		patch := client.RawPatch(types.JSONPatchType, p)
		r.Log.Info("removing finalizer from namespace")
		if err := r.Patch(ctx, ns, patch); err != nil {
			return ctrl.Result{}, err
		}
	}
}