One of the things that’s nice bout working with post types – custom or standard – in WordPress is that it’s really easy to hook into the serialization process in order to handle the data. This means that we have the ability to sanitize, format, read, access, modify, verify, etc. all of the data with the post type and with the post type’s meta data before it’s written to the database.

WordPress has a pretty consistent way of displaying error messages throughout the application. Really, it’s pretty consistent in how it displays all types of messages – success, updates, and errors – throughout the  system.

Let’s say that you’re working with a WordPress meta box, several of its fields are required, and you want to:

  • verify the input has been specified
  • either display an error message if it’s not specified
  • or write the data to the database if it checks out

The serialization process is pretty standard stuff, but if you’re looking to make sure required fields aren’t empty and that a error message is displayed whenever they’re not entered, then you’ll need to do some additional work.

Post Meta Data Error Messages

First, I want to mention that I know it’s possible to write code that will double-check the presence of inputs and then will render the error messages appropriately within the context of the meta box, but I dislike this for two reasons:

  1. It hijacks the experience that users are used to having within the Dashboard. That is, it displays messages where they aren’t normally displayed.
  2. If the meta box is located too low down the page and the message is displayed in said area, then the user may not see it at all.

Yes, the second point is really more of a consequence of the first but it’s still something worth considering especially since we’re using displays that range from, say, 11″ to 21″ (or more). And that’s the rationale behind why the following code does not display the errors in the meta box, but in the standard messaging area.

Location of the Death Star

Location of the Death Star

Anyway, for the purposes of this example, we’ll be looking at a Location post type and meta data for the death_star_plans.

1. Save the Location

First, we need to hook into the save_post action so that we can work with the meta data that’s associated with the post. For this example, assume that we’re working with an location post type and a piece of associated post meta data, say, death_star_plans.

The code above (as with the rest of the code in this post) is heavily commented in order to make it easy to follow, but to make sure we’re on the same page: In the gist, we’re hooking into the save process, verifying that we’re working our location post type and that the user has permission to save the data.

If not, then we stop the function from executing.

If so, then we proceed by calling a function responsible for evaluating the presence of the meta data and handling whether or not it should be saved or an error should be displayed.

2. Verify the Data Exists

As seen in the gist above, we need to check to see if the meta data exists. If so, then we’ll save it; otherwise, we need to display an error message:

Note that this could can be as simple or as complex as you’d like. Obviously, I’m just checking to see if the value in the $_POST array is set; however, there may be additional checks to add (such as if it’s of a certain length, if it’s only a number, etc.) depending on the type of meta data you’re saving.

3. Save the Plans

If we hit this point, then the presence of the data has passed so we can go ahead and save this to the database:

As commented, I’m only doing a very base-level of sanitization in this post. Again, this can be as complex as you like, but I’ve kept it simple for the purpose of this post.

4. Add the Errors

On the other hand, if the Plan meta data is not specified, then we need to add an error message to the list of error messages to be displayed on the post editor dashboard:

Here, we’re adding the data to WordPress’ settings error message collection. Since we’re not actually on a settings page, but on a post editor page, we need to set a transient so that WordPress will pick it up when it checks for messages. Finally, we remove this function from the admin_notice hook so it doesn’t fire again.

5. Display the Errors

Finally, if the errors exist, then we need to display them:

To do this, we simply check the transient value. If it doesn’t exist, then we stop the function; otherwise, we build up the list of errors, write it out to the display, and clear them so they aren’t displayed more than once.

The How and Why

As mentioned several times through this post, this code can be as simple or complex as you’d like. It’s not so much about what the code is doing. Instead, it’s about how to actually go about doing this in a clean, maintainable way.


Join the conversation! 13 Comments

  1. Thanks, Tom.


    if ( ‘firm’ != $_POST[‘post_type’]

    should be:

    if ( ‘location’ != $_POST[‘post_type’]


  2. You should use the hook save_post_$post_type. That save the internal check.
    But add a check for is_multisite() && ms_is_switched(), or you will update the wrong post meta in a multisite with synchronized posts (multiple calls to wp_insert_post() per save submit).

    The error message should be stored in a user meta, because it could show up for another user who is editing another post of the same type.

    • These are all great – I’ve updated the code to reflect the save_post hook (if anyone is still working with installations prior to 3.7 – and there are people who are), then they’d have to use the internal check.

      As far as multisite is concerned, that’s also good code. I rarely do anything with multisite so a lot of the stuff I share is more focused on single installs but I’ve updated to post to make sure to refer to this comment.

      The error message should be stored in a user meta, because it could show up for another user who is editing another post of the same type.

      This is a use case I hadn’t considered (I know, I know), but you’re right. I’ll have the gists updated in a bit.

      Thanks Thomas – appreciate this feedback :).

  3. Thanks, this post was helpful. I had tried using a glocal $notices before but it kept getting emptied until I used the transient method.

    many thanks!

  4. Hey Tom

    This is great :)

    I have tweaked it so I validate all of the fields at once like this:





    add_settings_error(‘missing-meta-box-difficulty’, ‘missing-meta-box-difficulty’, ‘You have not selected a difficulty.’, ‘error’);


    add_settings_error('missing-meta-box-preptime', 'missing-meta-box-preptime', 'You have not entered a numeric Prep Time.', 'error');

    Then at the end:


    set_transient( ‘settings_errors’, get_settings_errors(), 30);




    This all works, but if the form is submitted and there’s an error, all of the form data is lost and has to be re-entered. Is there a way round this?


    • First, check out the WordPress Coding Standards as the spacing of your code could be improved to make it more readable. Also be sure to read the section on Yoda Conditions as it will bring your conditionals up the ‘the WordPress way’ of writing them.

      In terms of saving information with the page refreshes, you’re going to have come up with a way to store the information such that you can re-populate the fields whenever the page refreshes. Since WordPress is stateless and session-less, this means that you’ll need to come up with another way of doing so.

      • You could store the information in a temporary place in the database, then populate the fields
      • You could implement a session mechanism to retrieve it from a PHP collection
      • …or something similar

      Other than that, the gist of what you’ll need to do is to store the data in a collection and then retrieve it after the page loads. How you go about doing that it totally up to you.

  5. Thanks Tom, sorry about the code formatting, it’s neatly laid out and tabbed in my editor I wasn’t sure how to format it in the forum post.


  6. Hi

    This is a great exposition of the “add_settings_error” feature, and I’ve been able to follow and implement everything… except.

    For some reason (unknown), get_transient( ‘settings_errors’ ) is returning a multi-dimensional array containing 3 (three) identical copies of the “add_settings_error” parameters expressed as an array.

    So my output looks like this:

    Array ([0] => Array ( [setting] => missing-death-star-plans => missing-death-star-plans [message] => You have not specified the location for the Death Star plans. [type] => error ) [1] => Array ([setting] => missing-death-star-plans => missing-death-star-plans [message] => You have not specified the location for the Death Star plans. [type] => error ) [2] => Array ( [setting] => missing-death-star-plans => missing-death-star-plans [message] => You have not specified the location for the Death Star plans. [type] => error ))

    And the foreach command dutifully repeats my message three times over.

    I don’t suppose you’ve seen this behaviour before, or you understand why it is happening.



    • I don’t suppose you’ve seen this behaviour before, or you understand why it is happening.

      Without seeing your project or knowing more, I’m afraid I can’t help but it sounds like the message may be added multiple times because the handler is because the handler is being called multiple times.

      • Are there other settings that are calling the same function for errors?
      • Is the function properly hooked in the sanitization process?

      I’d begin to isolate the options one by one and then trace the code to see how many times that function is called (because it’s clearly being called multiple times) and then begin addressing the areas in which the code is being called multiple times.

      • Very helpful advice.

        I had forgotten that I had an add_action for each of edit_post and publish_post (from my original code); add this to save_post equals three actions and therefore three notices. Once I whittled it down to just _save_post, then I got just a single error message.

        Case closed!

Leave a Reply