I scream, you scream, we all scream for @
@, no scream operator, is a classic PHP operator, meant to suppress error displays locally. It is a staple of PHP code, in use in over 65% of PHP repositories : 2 repositories out of 3. Yet, it is widely recommended avoiding using it.
Indeed, are there good reasons to still use this operator nowadays? @ is very slow : it actually intervenes at the last moment of errors processing. The error is processed by PHP, in particular the error handlers, and @ simply suppress any final output. This is the longest way to do nothing. In fact, it is often faster to write a check before the call, to alleviate that problem.
In order to understand the @ phenomenon better, let’s review a large range of situations, consider what is happening and what are the alternative options.
Obsolete Features
Here, @ is used to hide obsolescence messages. During the phase of ‘deprecated warning’, array_key_exists
works as usual, yet emits a warning to help prepare for the upcoming exception. In PHP 8.0, this call will emit a TypeError exception, and stop. Until then, a simple PHP Deprecated
is emitted, and the code will not stop.
<?php array_key_exists($key, $variable); ?>
The easy alternative is to configure php.ini directive error_reporting without deprecation notices. Yet, in the development phase, it is always better to keep the notice activated : this is the best moment to learn about then, and fix them.
In this case, the best alternative is to check that arraykeyexists() is actually applied to an array, and not an object. This is achieved in different ways : use of is_array() function with an if/then condition; use of (array) cast or even a typehint.
Upcoming Features
Deprecated features are usually warned before the actual removal. In case the code is trying to make use an upcoming PHP features, there are not early warning.
For example, this is also the case for upcoming new constant, @ENT_DISALLOWED, which appeared in PHP 5.4. Until then, using it is not possible, as htmlentities() will not recognize it.
In that case, a conditional constant (or other structures) definition, with a neutral value, removes the error. If the feature can be emulated, it is even better.
For Logging Purposes
Take a look at this case :
<?php @trigger_error('The use of the "default" keyword is deprecated. Use the real template name instead.', E_USER_DEPRECATED); ?>
Let’s understand it : trigger_error() emits an error and then, immediately, @ hide it. What is this apparent paradox?
trigger_error() is used here to fill a log, and @ is here to avoid such notice to be displayed in the GUI of the application. The deprecation should be hidden, indeed. Since the application uses error manager, the most logical way to log something is to emit an error, with the right level : here, E_USER_DEPRECATED
.
Among the alternative for this situation, one may use assert(). Those assertion produce an error, and may be removed with a php.ini directive. That way, the code is useful during the development phase, with its message, and removed in production. No more need for @
.
Another alternative is to switch to PHP 8.0 attributes (unless you are reading this article before PHP 8.0 publication).
Suppressing Errors In Eval()?
King of evil, eval() is definitely a classic candidate for error suppression. The problem is not related to eval() itself : after all, passing one string as argument is not a real challenge.
The real challenge is the PHP code inside the string, which will bubble up to the calling context.
<?php @eval('trigger_error("Yeah");'); ?>
Eval() is usually easy to avoid, in particular with modern PHP versions. You can check The Land Where PHP Uses eval() to get help on when or how to remove its usage.
It is good to remember that an eval() call should be placed in a try / catch block, so as to capture compilation problems. At least, that will be one problem less with eval(), and also, less feedback for any intruder.
Persisting Errors
Another situation arises with the usage of system calls, for example with shell_exec(). With the code below, the goal is to avoid displaying the system errors. It fails miserably.
<?php @shell_exec('ls non_existent_folder'); ?>
As soon as the shell is executed, the error channel is used for the errors that are generated in the shell. This channel is independent from the PHP error management, but merged with the PHP error channel. Here, adding the @ will not prevent the error display.
One alternative is to check the shell command, and apply an adapted verification. Here, the situation is simple enough : fileexists() and isdir() give us ways to avoid the call to shell_exec() and its error.
The other alternative is to merge the error channel with the output channel, in the shell. Like this :
<?php shell_exec('ls non_existent_folder 2&>1'); ?>
Now, the error is channeled to the result of shell_exec(), and not displayed immediately.
To Skip A Check
@ is quite often a sign that a check for existence has been skipped. Said another way, the code is comfortable with a missing value.
This is often the case with silenced values :
<?php echo @$raList[$cnt]; ?>
Here, the index is controlled by a variable. So, the index may be missing, or the variable $cnt
may contain an invalid or unexpected index.
Note also that @ is applied to the whole expression, so $raList
might also be non-existing, in that case. It is not possible to distinguish where the error occurs, between the variables or the index. We’ll see that feature later again.
The alternative to @ is isset(), empty() or the more recent ??. isset() checks that the value actually exists, while empty() checks if it exists and is not falsy. Null coalesce operator ?? provides a default value, in case the first expression is null, while it suppresses the error message.
<?php if (isset($raList[$cnt])) { echo $raList[$cnt]; } else { echo 'default'; } echo @$raList[$cnt] ?? 'default'; ?>
Another example of error suppression with a side effect is setcookie() or header(). Both of them have to be used before any output happens. You may have encountered the infamous error ‘headers already sent’.
<?php @setcookie('DEBUG', true); ?>
The alternative to @ is to make a call to headers_sent(). While the other examples achieved the task they were given, setcookie() and header() fail. @ makes this silent, and that is the beginning of a long bug hunt.
While checking on data existence is a good thing, the same applies to a number of external entities, such as files or remote applications. For example :
<?php @file_get_contents($object->url); @unlink($object->_tempFileName); ?>
Unlink() emits a Warning, if the soon-to-be removed file is missing. So, the existence check not be isset() but a file_exists() call. On the other hand, remote URL are difficult to check, before actually reaching them. In such situation, @ might be the easiest local solution.
To Cover A Whole File
@ suppresses the error messages during the whole expression it is assigned to. Most of the time, this is an expression, so the two strings below are actually hiding the same error :
<?php echo @strtoupper(substr(1234567, 2, 3)); echo strtoupper(@substr(1234567, 2, 3)); ?>
@ gets an extended power when applied to include(), and its cousins : includeonce(), require(), requireonce(). The error suppression is also applied to all the code that is included. This is interesting to understand, in particular when the inclusion is a constant expression, without any variable or function call.
<?php @include './functions.php' ?>
When the @ aims at checking if the included file is actually available, as in caching situations, then file_exists() checks removes the usage of @ on include.
It is very rare that error suppression is intended on a whole included file, so @ is probably superfluous after the file_exists() check.
Most Common Functions With @
What are the most silenced PHP functions? Here is a list of the classics, in alphabetical order (No need to make this a competition, right?).
- chmod
- copy
- define
- fclose
- file_exists
- file_get_contents
- file_put_contents
- filemtime
- fopen
- fread
- fseek
- fsockopen
- ftell
- fwrite
- get_image_size
- header
- iconv
- implode
- include
- include_once
- ini_get
- ini_set
- is_dir
- is_file
- list
- mkdir
- opendir
- preg_match
- rename
- rewind
- rmdir
- set_time_limit
- trigger_error
- unlink
- unserialize
Function about files are the most frequent, given that the involved files might or not be accessible at execution.
list() is a classic when the right-hand expression doesn’t contain enough elements to fill the left-hand expression. preg_match() is a bit of a surprise, just like the directive-related functions. define() illustrate a tactical fallback creation of constants.
A Special Case
While researching for this article, one special case fell on the radar. This simple preg_match() call, in Exakat’s code.
<?php @preg_match($regex, ''); ?>
It is actually the heart of an analysis rule Invalid regex. This analysis extracts regular expressions from the code, and check if it is a valid regex. The goal here is to make preg_match() detects regex compilation errors, catch them, hide them and then report it.
Obviously, it is a very niche usage for the @ operator, but it is worth mentioning.
For No Reasons At All
Finally, there is always some creative usage of @. They are rare, but always entertaining to find. One can only wonder how they ended up in the code.
<?php @phpversion( ); $a = @[]; $b = @@$sql; ?>
Let’s Stop Using @
@ has been refactored in recent PHP versions to ensure that it doesn’t catch any error that the directive ‘display_errors’ wouldn’t. Given that in the vast majority of cases, there are tools in PHP to check any resource, data or object and its existence, the last use case for @ is when one writes code too fast.
Indeed, while writing the stats, I ended up using @$stats[$function]++;
, to collect usage of functions. It was not tempting to check the existence of the index, and, moreover, as you read it, this code is now gone. Otherwise, it would have been extended with a clean check. That’s the real difference between quick and dirty @, and long-lasting code.