Introduction
Getting Started
- QuickStart
Patterns
- Languages
- Supported Languages
- Python
- Java
- JavaScript
- TypeScript
- Node.js
- React
- Fastify
- Next.js
- Terraform
- C#
- C++
- C
- Go
- Rust
- Swift
- React Native
- Spring Boot
- Kotlin
- Flutter
- Ruby
- PHP
- Scala
- Perl
- R
- Dart
- Elixir
- Erlang
- Haskell
- Lua
- Julia
- Clojure
- Groovy
- Fortran
- COBOL
- Pascal
- Assembly
- Bash
- PowerShell
- SQL
- PL/SQL
- T-SQL
- MATLAB
- Objective-C
- VBA
- ABAP
- Apex
- Apache Camel
- Crystal
- D
- Delphi
- Elm
- F#
- Hack
- Lisp
- OCaml
- Prolog
- Racket
- Scheme
- Solidity
- Verilog
- VHDL
- Zig
- MongoDB
- ClickHouse
- MySQL
- GraphQL
- Redis
- Cassandra
- Elasticsearch
- Security
- Performance
Integrations
- Code Repositories
- Team Messengers
- Ticketing
Enterprise
Erlang is a general-purpose, concurrent, functional programming language designed for building massively scalable, soft real-time systems with high availability requirements.
Erlang, despite being a powerful language for building concurrent and distributed systems, has several common anti-patterns that can lead to performance issues, maintainability problems, and bugs. Here are the most important anti-patterns to avoid when writing Erlang code.
%% Anti-pattern: Using processes unnecessarily
start() ->
spawn(fun() -> calculator_loop() end).
calculator_loop() ->
receive
{add, A, B, Caller} ->
Caller ! {result, A + B},
calculator_loop();
{subtract, A, B, Caller} ->
Caller ! {result, A - B},
calculator_loop()
end.
%% Usage
use_calculator() ->
Calc = start(),
Calc ! {add, 2, 3, self()},
receive
{result, Result} -> Result
end.
%% Better approach: Use regular functions for simple operations
add(A, B) -> A + B.
subtract(A, B) -> A - B.
%% Usage
use_calculator() ->
add(2, 3).
Don’t use processes for simple operations that don’t need concurrency, state isolation, or fault tolerance. Processes have overhead and should be used when their benefits are needed.
%% Anti-pattern: Reinventing OTP patterns
-module(user_store).
-export([start/0, store/3, retrieve/2]).
start() ->
spawn(fun() -> loop(#{}) end).
store(Pid, Key, Value) ->
Pid ! {store, Key, Value}.
retrieve(Pid, Key) ->
Pid ! {retrieve, Key, self()},
receive
{value, Value} -> Value
after 1000 ->
{error, timeout}
end.
loop(State) ->
receive
{store, Key, Value} ->
loop(maps:put(Key, Value, State));
{retrieve, Key, Caller} ->
Caller ! {value, maps:get(Key, State, undefined)},
loop(State)
end.
%% Better approach: Use gen_server
-module(user_store).
-behaviour(gen_server).
%% API
-export([start_link/0, store/2, retrieve/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
store(Key, Value) ->
gen_server:cast(?MODULE, {store, Key, Value}).
retrieve(Key) ->
gen_server:call(?MODULE, {retrieve, Key}).
%% Callbacks
init([]) ->
{ok, #{}}.
handle_call({retrieve, Key}, _From, State) ->
{reply, maps:get(Key, State, undefined), State}.
handle_cast({store, Key, Value}, State) ->
{noreply, maps:put(Key, Value, State)}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
Use OTP behaviors like gen_server, supervisor, and application instead of reinventing them. They provide battle-tested solutions for common patterns.
%% Anti-pattern: Inefficient list concatenation
process_items(Items) ->
process_items(Items, []).
process_items([], Acc) ->
Acc;
process_items([Item | Rest], Acc) ->
Processed = process_item(Item),
process_items(Rest, Acc ++ [Processed]). %% Inefficient: O(n) operation
%% Better approach: Use list prepending and reverse
process_items(Items) ->
process_items(Items, []).
process_items([], Acc) ->
lists:reverse(Acc); %% Reverse once at the end
process_items([Item | Rest], Acc) ->
Processed = process_item(Item),
process_items(Rest, [Processed | Acc]). %% Efficient: O(1) operation
Avoid using ++
for list concatenation in loops. Instead, use list prepending ([Head | Tail]
) and reverse the result at the end if order matters.
%% Anti-pattern: Not using pattern matching
process_response(Response) ->
case Response of
{ok, Body} ->
%% Process data
{ok, process_body(Body)};
{error, Reason} ->
%% Handle error
{error, Reason}
end.
%% Better approach: Use pattern matching in function heads
process_response({ok, Body}) ->
%% Process data
{ok, process_body(Body)};
process_response({error, Reason}) ->
%% Handle error
{error, Reason}.
Leverage Erlang’s pattern matching for cleaner, more declarative code. Use it in function heads and case statements.
%% Anti-pattern: Inefficient string operations
join_strings(Strings) ->
join_strings(Strings, "").
join_strings([], Acc) ->
Acc;
join_strings([String | Rest], Acc) ->
join_strings(Rest, Acc ++ String). %% Inefficient for strings
%% Better approach: Use binaries for strings
join_binaries(Binaries) ->
join_binaries(Binaries, <<>>).
join_binaries([], Acc) ->
Acc;
join_binaries([Binary | Rest], Acc) ->
join_binaries(Rest, <<Acc/binary, Binary/binary>>).
Use binaries instead of lists for string operations. Binaries are more efficient for string manipulation in Erlang.
%% Anti-pattern: Using tuples for structured data
create_user(Name, Email, Age) ->
{user, Name, Email, Age}.
get_user_name({user, Name, _Email, _Age}) ->
Name.
%% Better approach: Use records
-record(user, {name, email, age}).
create_user(Name, Email, Age) ->
#user{name = Name, email = Email, age = Age}.
get_user_name(#user{name = Name}) ->
Name.
%% Or even better in modern Erlang: Use maps
create_user(Name, Email, Age) ->
#{type => user, name => Name, email => Email, age => Age}.
get_user_name(#{type := user, name := Name}) ->
Name.
Use records or maps for structured data instead of raw tuples. They provide named fields and better pattern matching.
%% Anti-pattern: Ignoring errors
read_file(Filename) ->
case file:read_file(Filename) of
{ok, Content} ->
Content;
{error, _Reason} ->
<<>> %% Silent failure with empty binary
end.
%% Better approach: Proper error handling
read_file(Filename) ->
case file:read_file(Filename) of
{ok, Content} ->
{ok, Content};
{error, Reason} ->
{error, {file_read_error, Filename, Reason}}
end.
Use proper error handling with tagged tuples ({ok, Result}
and {error, Reason}
) and propagate errors up the call stack.
%% Anti-pattern: Excessive use of try/catch
divide(A, B) ->
try
A / B
catch
error:badarith ->
0
end.
%% Better approach: Use pattern matching and guards
divide(A, B) when B /= 0 ->
{ok, A / B};
divide(_A, 0) ->
{error, division_by_zero}.
Avoid using try/catch
for control flow. In Erlang, it’s more idiomatic to use pattern matching, guard clauses, and tagged tuples for error handling.
%% Anti-pattern: Manual process management
start_system() ->
DatabasePid = database:start_link(),
CachePid = cache:start_link(),
ApiPid = api:start_link(DatabasePid, CachePid),
{ok, ApiPid}.
%% What if one of these crashes?
%% Better approach: Use supervision trees
-module(my_app_sup).
-behaviour(supervisor).
-export([start_link/0, init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
SupFlags = #{strategy => one_for_one, intensity => 10, period => 60},
ChildSpecs = [
#{id => database, start => {database, start_link, []}, restart => permanent, shutdown => 5000, type => worker},
#{id => cache, start => {cache, start_link, []}, restart => permanent, shutdown => 5000, type => worker},
#{id => api, start => {api, start_link, []}, restart => permanent, shutdown => 5000, type => worker}
],
{ok, {SupFlags, ChildSpecs}}.
Use proper supervision trees to manage process lifecycles and handle failures. This is a core strength of the Erlang VM.
%% Anti-pattern: receive without timeout
get_data(Pid) ->
Pid ! {get_data, self()},
receive
{data, Data} -> Data
end. %% Could block forever
%% Better approach: Always use timeouts with receive
get_data(Pid) ->
Pid ! {get_data, self()},
receive
{data, Data} -> {ok, Data}
after 5000 ->
{error, timeout}
end.
Always use timeouts with receive
to prevent processes from blocking indefinitely if the expected message never arrives.
%% Anti-pattern: Using processes for simple shared state
-module(counter).
-export([start/0, increment/1, get/1]).
start() ->
spawn(fun() -> loop(0) end).
increment(Pid) ->
Pid ! increment,
ok.
get(Pid) ->
Pid ! {get, self()},
receive
{count, Count} -> Count
after 1000 ->
{error, timeout}
end.
loop(Count) ->
receive
increment ->
loop(Count + 1);
{get, Pid} ->
Pid ! {count, Count},
loop(Count)
end.
%% Better approach: Use ETS for shared state
-module(counter).
-export([start/0, increment/0, get/0]).
-define(TABLE, counter_table).
start() ->
ets:new(?TABLE, [named_table, public]),
ets:insert(?TABLE, {count, 0}),
ok.
increment() ->
ets:update_counter(?TABLE, count, 1),
ok.
get() ->
[{count, Count}] = ets:lookup(?TABLE, count),
Count.
Use ETS (Erlang Term Storage) tables for shared state when appropriate, especially for read-heavy scenarios or when low-latency access is required.
%% Anti-pattern: Poor module structure
-module(user_system).
-export([create_user/1, update_user/2, delete_user/1, get_user/1, list_users/0,
authenticate_user/2, generate_password_reset_token/1]).
%% Hundreds of functions in one module
create_user(Params) -> % ...
update_user(Id, Params) -> % ...
delete_user(Id) -> % ...
get_user(Id) -> % ...
list_users() -> % ...
authenticate_user(Email, Password) -> % ...
generate_password_reset_token(Email) -> % ...
%% And many more...
%% Better approach: Proper module organization
%% users.erl
-module(users).
-export([create/1, update/2, delete/1, get/1, list/0]).
create(Params) -> % ...
update(Id, Params) -> % ...
delete(Id) -> % ...
get(Id) -> % ...
list() -> % ...
%% users_auth.erl
-module(users_auth).
-export([authenticate/2, generate_password_reset_token/1]).
authenticate(Email, Password) -> % ...
generate_password_reset_token(Email) -> % ...
Organize your code into cohesive modules with clear responsibilities. Follow the principle of single responsibility and create a logical hierarchy of modules.
%% Anti-pattern: Poor or no documentation
-module(user_api).
-export([create/1]).
create(Params) ->
%% Implementation...
{ok, User}.
%% Better approach: Proper documentation
-module(user_api).
-export([create/1]).
%% @doc Creates a new user with the given parameters.
%%
%% Params is a map containing user attributes:
%% <ul>
%% <li>`name': The user's full name (required)</li>
%% <li>`email': The user's email address (required)</li>
%% <li>`age': The user's age (optional)</li>
%% </ul>
%%
%% Examples:
%% ```
%% {ok, User} = user_api:create(#{name => "John Doe", email => "john@example.com"}).
%% {error, email_required} = user_api:create(#{name => "John Doe"}).
%% '''
%%
%% @returns `{ok, User}' if the user was created successfully, or
%% `{error, Reason}' if there was an error.
-spec create(map()) -> {ok, map()} | {error, atom()}.
create(Params) ->
%% Implementation...
{ok, User}.
Write comprehensive documentation for your modules and functions using EDoc comments. Include examples, parameter descriptions, and return value information.
%% Anti-pattern: No type specs
-module(calculator).
-export([add/2, divide/2]).
add(A, B) ->
A + B.
divide(A, B) ->
A / B.
%% Better approach: Use type specs
-module(calculator).
-export([add/2, divide/2]).
-spec add(number(), number()) -> number().
add(A, B) ->
A + B.
-spec divide(number(), number()) -> float() | {error, division_by_zero}.
divide(A, B) when B /= 0 ->
A / B;
divide(_A, 0) ->
{error, division_by_zero}.
Use Dialyzer and type specifications to catch type errors at compile time and improve code documentation.
%% Anti-pattern: Ad-hoc application structure
%% Just a collection of modules without proper OTP structure
%% Better approach: Proper OTP application structure
%% my_app.app.src
{application, my_app, [
{description, "My Application"},
{vsn, "0.1.0"},
{registered, []},
{mod, {my_app_app, []}},
{applications, [kernel, stdlib]},
{env, []},
{modules, []}
]}.
%% my_app_app.erl
-module(my_app_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
my_app_sup:start_link().
stop(_State) ->
ok.
%% my_app_sup.erl
-module(my_app_sup).
-behaviour(supervisor).
-export([start_link/0, init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
SupFlags = #{strategy => one_for_one, intensity => 10, period => 60},
ChildSpecs = [
%% Child specifications
],
{ok, {SupFlags, ChildSpecs}}.
Follow the OTP application structure with proper application, supervisor, and worker modules.
%% Anti-pattern: Not registering important processes
start() ->
Pid = spawn(fun() -> loop([]) end),
Pid. %% Caller needs to keep track of the Pid
%% Usage
use_service() ->
Pid = start(),
Pid ! {get_data, self()},
receive
{data, Data} -> Data
end.
%% Better approach: Register important processes
start() ->
Pid = spawn(fun() -> loop([]) end),
register(my_service, Pid),
{ok, Pid}.
%% Usage
use_service() ->
my_service ! {get_data, self()},
receive
{data, Data} -> Data
end.
Register important long-lived processes with names for easier access. This is especially important for system processes that need to be accessed from multiple places.
%% Anti-pattern: Ambiguous message patterns
loop(State) ->
receive
{get, Key} ->
Value = maps:get(Key, State, undefined),
%% Who sent this message? No way to reply
loop(State);
{set, Key, Value} ->
loop(maps:put(Key, Value, State))
end.
%% Better approach: Include sender in messages
loop(State) ->
receive
{get, Key, From} ->
Value = maps:get(Key, State, undefined),
From ! {response, Value},
loop(State);
{set, Key, Value, From} ->
From ! ok,
loop(maps:put(Key, Value, State))
end.
Include the sender’s PID in messages that require a response. This allows the receiving process to reply to the correct sender.
%% Anti-pattern: Using lists:foreach for transformations
process_items(Items) ->
Results = [],
lists:foreach(fun(Item) ->
Result = process_item(Item),
Results = [Result | Results] %% This doesn't work as expected!
end, Items),
lists:reverse(Results).
%% Better approach: Use list comprehensions or lists:map
process_items(Items) ->
[process_item(Item) || Item <- Items].
%% Or
process_items(Items) ->
lists:map(fun process_item/1, Items).
Use list comprehensions or lists:map
for transforming lists. lists:foreach
is only for side effects and doesn’t return a useful value.
%% Anti-pattern: Manual testing
test() ->
5 = add(2, 3),
io:format("Test passed!~n").
%% Better approach: Use EUnit
-module(calculator_tests).
-include_lib("eunit/include/eunit.hrl").
add_test() ->
?assertEqual(5, calculator:add(2, 3)),
?assertEqual(0, calculator:add(-2, 2)),
?assertEqual(-5, calculator:add(-2, -3)).
divide_test() ->
?assertEqual(2.5, calculator:divide(5, 2)),
?assertEqual({error, division_by_zero}, calculator:divide(5, 0)).
Use proper testing frameworks like EUnit or Common Test instead of writing manual test code.