I also needed labeled edges for TikZ chains, but needed to retain the features of the original join=with <node> by <options> syntax. So, I wrote some code to admit the syntax join=with <node> by <options> nodes <node path>. Here is that code, along with an example:
\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{chains}
\makeatletter
\def\tikz@lib@parse@join#1{%
\def\tikz@temp{#1}%
\ifx\tikz@temp\pgfutil@empty%
\tikz@lib@join@by@nodes by {} nodes \pgfutil@stop%
\else%
\pgfutil@in@{nodes }{#1}%
\ifpgfutil@in@%
\tikz@lib@parse@join@nodes#1\pgfutil@stop%
\else%
\tikz@lib@parse@join@nodes#1 nodes \pgfutil@stop%
\fi%
\fi%
}
\def\tikz@lib@parse@join@nodes#1nodes #2\pgfutil@stop{%
\pgfutil@in@{by }{#1}%
\ifpgfutil@in@%
\tikz@lib@parse@join@by#1nodes #2\pgfutil@stop%
\else%
\tikz@lib@parse@join@by#1by {} nodes #2\pgfutil@stop%
\fi%
}
\def\tikz@lib@parse@join@by#1by #2 nodes #3\pgfutil@stop{%
\pgfutil@in@{with }{#1}%
\ifpgfutil@in@%
\tikz@lib@join@with@by@nodes#1by #2 nodes #3\pgfutil@stop%
\else%
\tikz@lib@join@by@nodes by #2 nodes #3\pgfutil@stop%
\fi%
}
\def\tikz@lib@join@with@by@nodes with #1 by #2 nodes #3\pgfutil@stop{%
\tikzset{after node path={(#1)edge[every join,#2]#3(\tikzchaincurrent)}}%
}
\def\tikz@lib@join@by@nodes by #1 nodes #2\pgfutil@stop{%
\tikzset{after node path={%
\ifx\tikzchainprevious\pgfutil@empty%
\else%
(\tikzchainprevious)edge[every join,#1]#2(\tikzchaincurrent)%
\fi}}%
}
\makeatother
\begin{document}
\begin{tikzpicture}[start chain, every join/.append style={->}]
\node [on chain, join] {A};
\node [on chain, join] {B};
\node [on chain, join=nodes {node [above] {$\gamma$}}] {C};
\node [on chain=going below,
join=by {red} nodes {node [right] {$\delta$}}] {D};
\node [continue chain=going left, on chain,
join,
join=with chain-begin by {<-, blue}
nodes {node [sloped,above] {$\epsilon$}},
join=with chain-2 by {|->, red}] {E};
\node [on chain, join=by {<->,green},
join=with chain-begin] {F};
\end{tikzpicture}
\end{document}

Explanation
\tikz@lib@parse@join Macro
\tikz@lib@parse@join first checks whether the argument to join is empty. If so, the join occurs with empty by and nodes components and no with component.
Otherwise, we check whether there is a nodes component. If not, we append an empty nodes component to #1; the space inserted between #1 and nodes is needed because #1 (is not empty and) does not end with a space. We then call \tikz@lib@parse@join@nodes.
\tikz@lib@parse@join@nodes Macro
Now, thanks to the previous macro, a (possibly empty) nodes component is guaranteed to exist. We next check whether there is a by component. If not, we insert an empty by component. We then call \tikz@lib@parse@join@by.
Note that, in case we must insert an empty by, we do not add a space between #1 and by. This is because either:
- there was only a
nodes component, in which case #1 is empty; or
- there was a
with component in addition to the nodes component, in which case #1 contains the with and itself ends with a space.
\tikz@lib@parse@join@by Macro
Now, thanks to the previous macros, (possibly empty) by and nodes components are guaranteed to exist. We next check whether there is a with component. If so, we call \tikz@lib@join@with@by@nodes. Otherwise, we call \tikz@lib@join@by@nodes.